From 6a489287ba6ec3eaec1889c84e16f918c65f7132 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Thu, 4 Jan 2018 23:47:05 +0300 Subject: [PATCH 01/26] Removed OldSkinId field --- worker/worder_test.go | 1 - worker/worker.go | 1 - 2 files changed, 2 deletions(-) diff --git a/worker/worder_test.go b/worker/worder_test.go index cc2f7b8..9bd9460 100644 --- a/worker/worder_test.go +++ b/worker/worder_test.go @@ -88,7 +88,6 @@ func TestServices_HandleSkinChanged(t *testing.T) { AccountId: 1, Uuid: "cdb907ce-84f4-4c38-801d-1e287dca2623", SkinId: 2, - OldSkinId: 1, Hash: "f76caa016e07267a05b7daf9ebc7419c", Is1_8: true, IsSlim: false, diff --git a/worker/worker.go b/worker/worker.go index 19702ad..fe26136 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -30,7 +30,6 @@ type SkinChanged struct { AccountId int `json:"userId"` Uuid string `json:"uuid"` SkinId int `json:"skinId"` - OldSkinId int `json:"oldSkinId"` Hash string `json:"hash"` Is1_8 bool `json:"is1_8"` IsSlim bool `json:"isSlim"` From d2485df64de055f95e40025f0af66ed1e36c68b5 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Fri, 5 Jan 2018 00:10:38 +0300 Subject: [PATCH 02/26] Use accepted PR of github.com/mono83/slf with support of Sentry logger --- Gopkg.lock | 6 +++--- bootstrap/bootstrap.go | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index f2630e1..6a85d80 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -82,8 +82,8 @@ [[projects]] branch = "master" name = "github.com/mono83/slf" - packages = [".","filters","params","rays","recievers","recievers/ansi","recievers/statsd","wd"] - revision = "8188a95c8d6b74c43953abb38b8bd6fdbc412ff5" + packages = [".","filters","params","rays","recievers","recievers/sentry","recievers/statsd","recievers/writer","wd"] + revision = "79153e9636db86e1c6b74d74dd04176f257a4f2d" [[projects]] branch = "master" @@ -184,6 +184,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "dd545fafc23f9b6429b5b679ad5c213c14c819f1e4ea381823acf338651122e1" + inputs-digest = "11938f85225b2839e4ed7cd4345bed8f44510b6eb50c003b89c8e14e0fd6b6e7" solver-name = "gps-cdcl" solver-version = 1 diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index d9c13ff..2dc9a15 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -8,11 +8,10 @@ import ( "github.com/assembla/cony" "github.com/getsentry/raven-go" "github.com/mono83/slf/rays" + "github.com/mono83/slf/recievers/sentry" "github.com/mono83/slf/recievers/statsd" "github.com/mono83/slf/recievers/writer" "github.com/mono83/slf/wd" - - "elyby/minecraft-skinsystem/logger/receivers/sentry" ) var version = "" From ca4479252febabc95f4430e0a2af3e55fed9067e Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Mon, 15 Jan 2018 23:52:22 +0300 Subject: [PATCH 03/26] Implemented jwt generation --- Gopkg.lock | 34 +++++++++++++- Gopkg.toml | 11 +++++ auth/jwt.go | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++ cmd/root.go | 1 + cmd/token.go | 65 +++++++++++++++++++++++++++ 5 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 auth/jwt.go create mode 100644 cmd/token.go diff --git a/Gopkg.lock b/Gopkg.lock index 6a85d80..5736959 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,6 +1,12 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. +[[projects]] + name = "github.com/SermoDigital/jose" + packages = [".","crypto","jws","jwt"] + revision = "f6df55f235c24f236d11dbcf665249a59ac2021f" + version = "1.1" + [[projects]] name = "github.com/assembla/cony" packages = ["."] @@ -55,6 +61,12 @@ packages = [".","hcl/ast","hcl/parser","hcl/scanner","hcl/strconv","hcl/token","json/parser","json/scanner","json/token"] revision = "8f6b1344a92ff8877cf24a5de9177bf7d0a2a187" +[[projects]] + branch = "master" + name = "github.com/howeyc/gopass" + packages = ["."] + revision = "bf9dde6d0d2c004a008c27aaee91170c786f6db8" + [[projects]] name = "github.com/inconshreveable/mousetrap" packages = ["."] @@ -73,6 +85,12 @@ packages = ["cluster","pool","redis","util"] revision = "d234cfb904a91daafa4e1f92599a893b349cc0c2" +[[projects]] + branch = "master" + name = "github.com/mitchellh/go-homedir" + packages = ["."] + revision = "b8bc1bf767474819792c23f32d8286a45736f1c6" + [[projects]] branch = "master" name = "github.com/mitchellh/mapstructure" @@ -109,6 +127,12 @@ revision = "792786c7400a136282c1664665ae0a8db921c6c2" version = "v1.0.0" +[[projects]] + branch = "master" + name = "github.com/segmentio/go-prompt" + packages = ["."] + revision = "f0d19b6901ade831d5a3204edc0d6a7d6457fbb2" + [[projects]] branch = "master" name = "github.com/spf13/afero" @@ -157,10 +181,16 @@ revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0" version = "v1.1.4" +[[projects]] + branch = "master" + name = "golang.org/x/crypto" + packages = ["ssh/terminal"] + revision = "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8" + [[projects]] branch = "master" name = "golang.org/x/sys" - packages = ["unix"] + packages = ["unix","windows"] revision = "7ddbeae9ae08c6a06a59597f0c9edbc5ff2444ce" [[projects]] @@ -184,6 +214,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "11938f85225b2839e4ed7cd4345bed8f44510b6eb50c003b89c8e14e0fd6b6e7" + inputs-digest = "a12e681ec671ce8a93256cd754d4e70797476b2d2ce4379c3860df09c4b6a552" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index da94f32..a1ba591 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -23,6 +23,17 @@ ignored = ["elyby/minecraft-skinsystem"] name = "github.com/assembla/cony" version = "^0.3.2" +[[constraint]] + name = "github.com/SermoDigital/jose" + version = "~1.1.0" + +[[constraint]] + name = "github.com/mitchellh/go-homedir" + +[[constraint]] + name = "github.com/segmentio/go-prompt" + branch = "master" + # Testing dependencies [[constraint]] diff --git a/auth/jwt.go b/auth/jwt.go new file mode 100644 index 0000000..8ed9f97 --- /dev/null +++ b/auth/jwt.go @@ -0,0 +1,124 @@ +package auth + +import ( + "encoding/base64" + "io/ioutil" + "math" + "math/rand" + "os" + "time" + + "github.com/SermoDigital/jose/crypto" + "github.com/SermoDigital/jose/jws" + "github.com/mitchellh/go-homedir" +) + +var hashAlg = crypto.SigningMethodHS256 + +const appHomeDirName = ".minecraft-skinsystem" +const scopesClaim = "scopes" + +type Scope string + +var ( + SkinScope = Scope("skin") +) + +type JwtAuth struct { + signingKey []byte +} + +func (t *JwtAuth) NewToken(scopes ...Scope) ([]byte, error) { + key, err := t.getSigningKey() + if err != nil { + return nil, err + } + + claims := jws.Claims{} + claims.Set(scopesClaim, scopes) + claims.SetIssuedAt(time.Now()) + encoder := jws.NewJWT(claims, hashAlg) + token, err := encoder.Serialize(key) + if err != nil { + return nil, err + } + + return token, nil +} + +func (t *JwtAuth) GenerateSigningKey() error { + if err := createAppHomeDir(); err != nil { + return err + } + + key := generateRandomBytes(64) + if err := ioutil.WriteFile(getKeyPath(), key, 0600); err != nil { + return err + } + + return nil +} + +func (t *JwtAuth) getSigningKey() ([]byte, error) { + if t.signingKey != nil { + return t.signingKey, nil + } + + path := getKeyPath() + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return nil, &SigningKeyNotAvailable{} + } + + return nil, err + } + + key, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + return key, nil +} + +func createAppHomeDir() error { + path := getAppHomeDirPath() + if _, err := os.Stat(path); os.IsNotExist(err) { + err := os.Mkdir(path, 0755) // rwx r-x r-x + if err != nil { + return err + } + } + + return nil +} + +func getAppHomeDirPath() string { + path, err := homedir.Expand("~/" + appHomeDirName) + if err != nil { + panic(err) + } + + return path +} + +func getKeyPath() string { + return getAppHomeDirPath() + "/jwt-key" +} + +func generateRandomBytes(n int) []byte { + randLen := int(math.Ceil(float64(n) / 1.37)) // base64 will increase length in 1.37 times + randBytes := make([]byte, randLen) + rand.Read(randBytes) + resBytes := make([]byte, n) + base64.URLEncoding.Encode(resBytes, randBytes) + + return resBytes +} + +type SigningKeyNotAvailable struct { +} + +func (*SigningKeyNotAvailable) Error() string { + return "Signing key not available" +} diff --git a/cmd/root.go b/cmd/root.go index 7c32742..d4b0df2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -41,6 +41,7 @@ func initConfig() { viper.AutomaticEnv() if err := viper.ReadInConfig(); err == nil { + // TODO: show only on verbose mode fmt.Println("Using config file:", viper.ConfigFileUsed()) } } diff --git a/cmd/token.go b/cmd/token.go new file mode 100644 index 0000000..a26f681 --- /dev/null +++ b/cmd/token.go @@ -0,0 +1,65 @@ +package cmd + +import ( + "fmt" + "log" + + "elyby/minecraft-skinsystem/auth" + + "github.com/segmentio/go-prompt" + "github.com/spf13/cobra" +) + +var tokenCmd = &cobra.Command{ + Use: "token", + Short: "API tokens operations", +} + +var createCmd = &cobra.Command{ + Use: "create", + Short: "Create the new token, that allows interacting with Ely.by Skinsystem API", + Run: func(cmd *cobra.Command, args []string) { + jwtAuth := &auth.JwtAuth{} + for { + token, err := jwtAuth.NewToken(auth.SkinScope) + if err != nil { + if _, ok := err.(*auth.SigningKeyNotAvailable); !ok { + log.Fatalf("Unable to create new token. The error is %v\n", err) + } + + log.Println("Signing key not available. Creating...") + err := jwtAuth.GenerateSigningKey() + if err != nil { + log.Fatalf("Unable to generate new signing key. The error is %v\n", err) + } + + continue + } + + fmt.Printf("%s\n", token) + } + }, +} + +var resetCmd = &cobra.Command{ + Use: "reset", + Short: "Regenerate the secret key, that invalidate all tokens", + Run: func(cmd *cobra.Command, args []string) { + if !prompt.Confirm("Do you really want to invalidate all exists tokens?") { + fmt.Println("Aboart.") + return + } + + jwtAuth := &auth.JwtAuth{} + if err := jwtAuth.GenerateSigningKey(); err != nil { + log.Fatalf("Unable to generate new signing key. The error is %v\n", err) + } + + fmt.Println("Token successfully regenerated.") + }, +} + +func init() { + tokenCmd.AddCommand(createCmd, resetCmd) + RootCmd.AddCommand(tokenCmd) +} From b8c3cc6cf8546db029353067bf1913c03e2294f0 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Sat, 20 Jan 2018 21:23:05 +0300 Subject: [PATCH 04/26] Added sh script to rebuild mocks for interfaces --- script/mocks | 4 ++++ 1 file changed, 4 insertions(+) create mode 100755 script/mocks diff --git a/script/mocks b/script/mocks new file mode 100755 index 0000000..d73d1ef --- /dev/null +++ b/script/mocks @@ -0,0 +1,4 @@ +#!/bin/sh + +mockgen -source=interfaces/repositories.go -destination=interfaces/mock_interfaces/mock_interfaces.go +mockgen -source=interfaces/api.go -destination=interfaces/mock_interfaces/mock_api.go From aaff88d32fdf83af0371d6717e744514ab5840d3 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Tue, 23 Jan 2018 00:16:42 +0300 Subject: [PATCH 05/26] Reworked http tests mocking --- http/cape_test.go | 28 ++++++++++++++-------------- http/face_test.go | 12 ++++++------ http/http_test.go | 18 +++++++++++++----- http/signed_textures_test.go | 12 ++++++------ http/skin_test.go | 29 +++++++++++++++-------------- http/textures_test.go | 32 ++++++++++++++++---------------- 6 files changed, 70 insertions(+), 61 deletions(-) diff --git a/http/cape_test.go b/http/cape_test.go index ed50c1e..1d3de99 100644 --- a/http/cape_test.go +++ b/http/cape_test.go @@ -21,14 +21,14 @@ func TestConfig_Cape(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - config, _, capesRepo, wd := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) cape := createCape() - capesRepo.EXPECT().FindByUsername("mocked_username").Return(&model.Cape{ + mocks.Capes.EXPECT().FindByUsername("mocked_username").Return(&model.Cape{ File: bytes.NewReader(cape), }, nil) - wd.EXPECT().IncCounter("capes.request", int64(1)) + mocks.Log.EXPECT().IncCounter("capes.request", int64(1)) req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks/mocked_username", nil) w := httptest.NewRecorder() @@ -48,10 +48,10 @@ func TestConfig_Cape2(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - config, _, capesRepo, wd := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - capesRepo.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{"notch"}) - wd.EXPECT().IncCounter("capes.request", int64(1)) + mocks.Capes.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{"notch"}) + mocks.Log.EXPECT().IncCounter("capes.request", int64(1)) req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks/notch", nil) w := httptest.NewRecorder() @@ -69,15 +69,15 @@ func TestConfig_CapeGET(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - config, _, capesRepo, wd := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) cape := createCape() - capesRepo.EXPECT().FindByUsername("mocked_username").Return(&model.Cape{ + mocks.Capes.EXPECT().FindByUsername("mocked_username").Return(&model.Cape{ File: bytes.NewReader(cape), }, nil) - wd.EXPECT().IncCounter("capes.request", int64(1)).Times(0) - wd.EXPECT().IncCounter("capes.get_request", int64(1)) + mocks.Log.EXPECT().IncCounter("capes.request", int64(1)).Times(0) + mocks.Log.EXPECT().IncCounter("capes.get_request", int64(1)) req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks?name=mocked_username", nil) w := httptest.NewRecorder() @@ -97,11 +97,11 @@ func TestConfig_CapeGET2(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - config, _, capesRepo, wd := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - capesRepo.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{"notch"}) - wd.EXPECT().IncCounter("capes.request", int64(1)).Times(0) - wd.EXPECT().IncCounter("capes.get_request", int64(1)) + mocks.Capes.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{"notch"}) + mocks.Log.EXPECT().IncCounter("capes.request", int64(1)).Times(0) + mocks.Log.EXPECT().IncCounter("capes.get_request", int64(1)) req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks?name=notch", nil) w := httptest.NewRecorder() diff --git a/http/face_test.go b/http/face_test.go index f61daff..8bb698d 100644 --- a/http/face_test.go +++ b/http/face_test.go @@ -16,10 +16,10 @@ func TestConfig_Face(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - config, skinsRepo, _, wd := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) - wd.EXPECT().IncCounter("faces.request", int64(1)) + mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) + mocks.Log.EXPECT().IncCounter("faces.request", int64(1)) req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/mock_user/face.png", nil) w := httptest.NewRecorder() @@ -37,10 +37,10 @@ func TestConfig_Face2(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - config, skinsRepo, _, wd := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - skinsRepo.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{"mock_user"}) - wd.EXPECT().IncCounter("faces.request", int64(1)) + mocks.Skins.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{"mock_user"}) + mocks.Log.EXPECT().IncCounter("faces.request", int64(1)) req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/mock_user/face.png", nil) w := httptest.NewRecorder() diff --git a/http/http_test.go b/http/http_test.go index be23234..3db459d 100644 --- a/http/http_test.go +++ b/http/http_test.go @@ -22,11 +22,15 @@ func TestBuildElyUrl(t *testing.T) { assert.Equal("http://ely.by/test/route", buildElyUrl("http://ely.by/test/route"), "Function should do not add prefix to the provided prefixed url.") } +type mocks struct { + Skins *mock_interfaces.MockSkinsRepository + Capes *mock_interfaces.MockCapesRepository + Log *mock_wd.MockWatchdog +} + func setupMocks(ctrl *gomock.Controller) ( *Config, - *mock_interfaces.MockSkinsRepository, - *mock_interfaces.MockCapesRepository, - *mock_wd.MockWatchdog, + *mocks, ) { skinsRepo := mock_interfaces.NewMockSkinsRepository(ctrl) capesRepo := mock_interfaces.NewMockCapesRepository(ctrl) @@ -35,6 +39,10 @@ func setupMocks(ctrl *gomock.Controller) ( return &Config{ SkinsRepo: skinsRepo, CapesRepo: capesRepo, - Logger: wd, - }, skinsRepo, capesRepo, wd + Logger: wd, + }, &mocks{ + Skins: skinsRepo, + Capes: capesRepo, + Log: wd, + } } diff --git a/http/signed_textures_test.go b/http/signed_textures_test.go index 48d728a..56465df 100644 --- a/http/signed_textures_test.go +++ b/http/signed_textures_test.go @@ -17,10 +17,10 @@ func TestConfig_SignedTextures(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - config, skinsRepo, _, wd := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) - wd.EXPECT().IncCounter("signed_textures.request", int64(1)) + mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) + mocks.Log.EXPECT().IncCounter("signed_textures.request", int64(1)) req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/signed/mock_user", nil) w := httptest.NewRecorder() @@ -54,10 +54,10 @@ func TestConfig_SignedTextures2(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - config, skinsRepo, _, wd := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - skinsRepo.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{}) - wd.EXPECT().IncCounter("signed_textures.request", int64(1)) + mocks.Skins.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{}) + mocks.Log.EXPECT().IncCounter("signed_textures.request", int64(1)) req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/signed/mock_user", nil) w := httptest.NewRecorder() diff --git a/http/skin_test.go b/http/skin_test.go index 0f55cb7..c58585d 100644 --- a/http/skin_test.go +++ b/http/skin_test.go @@ -17,10 +17,10 @@ func TestConfig_Skin(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - config, skinsRepo, _, wd := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) - wd.EXPECT().IncCounter("skins.request", int64(1)) + mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) + mocks.Log.EXPECT().IncCounter("skins.request", int64(1)) req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/mock_user", nil) w := httptest.NewRecorder() @@ -38,10 +38,10 @@ func TestConfig_Skin2(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - config, skinsRepo, _, wd := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - skinsRepo.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{"notch"}) - wd.EXPECT().IncCounter("skins.request", int64(1)) + mocks.Skins.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{"notch"}) + mocks.Log.EXPECT().IncCounter("skins.request", int64(1)) req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/notch", nil) w := httptest.NewRecorder() @@ -59,11 +59,11 @@ func TestConfig_SkinGET(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - config, skinsRepo, _, wd := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) - wd.EXPECT().IncCounter("skins.get_request", int64(1)) - wd.EXPECT().IncCounter("skins.request", int64(1)).Times(0) + mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) + mocks.Log.EXPECT().IncCounter("skins.get_request", int64(1)) + mocks.Log.EXPECT().IncCounter("skins.request", int64(1)).Times(0) req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins?name=mock_user", nil) w := httptest.NewRecorder() @@ -81,11 +81,11 @@ func TestConfig_SkinGET2(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - config, skinsRepo, _, wd := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - skinsRepo.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{"notch"}) - wd.EXPECT().IncCounter("skins.get_request", int64(1)) - wd.EXPECT().IncCounter("skins.request", int64(1)).Times(0) + mocks.Skins.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{"notch"}) + mocks.Log.EXPECT().IncCounter("skins.get_request", int64(1)) + mocks.Log.EXPECT().IncCounter("skins.request", int64(1)).Times(0) req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins?name=notch", nil) w := httptest.NewRecorder() @@ -112,6 +112,7 @@ func TestConfig_SkinGET3(t *testing.T) { func createSkinModel(username string, isSlim bool) *model.Skin { return &model.Skin{ + UserId: 1, Username: username, Uuid: "0f657aa8-bfbe-415d-b700-5750090d3af3", SkinId: 1, diff --git a/http/textures_test.go b/http/textures_test.go index 97f6ac2..9fdbe09 100644 --- a/http/textures_test.go +++ b/http/textures_test.go @@ -20,11 +20,11 @@ func TestConfig_Textures(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - config, skinsRepo, capesRepo, wd := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) - capesRepo.EXPECT().FindByUsername("mock_user").Return(nil, &db.CapeNotFoundError{"mock_user"}) - wd.EXPECT().IncCounter("textures.request", int64(1)) + mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) + mocks.Capes.EXPECT().FindByUsername("mock_user").Return(nil, &db.CapeNotFoundError{"mock_user"}) + mocks.Log.EXPECT().IncCounter("textures.request", int64(1)) req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/mock_user", nil) w := httptest.NewRecorder() @@ -49,11 +49,11 @@ func TestConfig_Textures2(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - config, skinsRepo, capesRepo, wd := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", true), nil) - capesRepo.EXPECT().FindByUsername("mock_user").Return(nil, &db.CapeNotFoundError{"mock_user"}) - wd.EXPECT().IncCounter("textures.request", int64(1)) + mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", true), nil) + mocks.Capes.EXPECT().FindByUsername("mock_user").Return(nil, &db.CapeNotFoundError{"mock_user"}) + mocks.Log.EXPECT().IncCounter("textures.request", int64(1)) req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/mock_user", nil) w := httptest.NewRecorder() @@ -81,13 +81,13 @@ func TestConfig_Textures3(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - config, skinsRepo, capesRepo, wd := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) - capesRepo.EXPECT().FindByUsername("mock_user").Return(&model.Cape{ + mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) + mocks.Capes.EXPECT().FindByUsername("mock_user").Return(&model.Cape{ File: bytes.NewReader(createCape()), }, nil) - wd.EXPECT().IncCounter("textures.request", int64(1)) + mocks.Log.EXPECT().IncCounter("textures.request", int64(1)) req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/mock_user", nil) w := httptest.NewRecorder() @@ -116,11 +116,11 @@ func TestConfig_Textures4(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - config, skinsRepo, capesRepo, wd := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - skinsRepo.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{}) - capesRepo.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{}) - wd.EXPECT().IncCounter("textures.request", int64(1)) + mocks.Skins.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{}) + mocks.Capes.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{}) + mocks.Log.EXPECT().IncCounter("textures.request", int64(1)) timeNow = func() time.Time { return time.Date(2017, time.August, 20, 0, 15, 54, 0, time.UTC) } From f120064fe393bb25f74e3b631f4c27948fb19c2e Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Tue, 23 Jan 2018 18:43:37 +0300 Subject: [PATCH 06/26] Implemented API endpoint to update skin information Added tests to jwt package Reworked redis backend implementation Skin repository now have methods to remove skins by user id or username --- Gopkg.lock | 9 +- Gopkg.toml | 9 + auth/jwt.go | 90 +++-- auth/jwt_test.go | 168 +++++++++ cmd/serve.go | 9 +- db/redis.go | 113 ++++-- http/api.go | 202 +++++++++++ http/api_test.go | 337 ++++++++++++++++++ http/http.go | 3 + http/http_test.go | 4 + interfaces/auth.go | 7 + interfaces/mock_interfaces/mock_interfaces.go | 24 ++ interfaces/repositories.go | 2 + script/mocks | 1 + 14 files changed, 923 insertions(+), 55 deletions(-) create mode 100644 auth/jwt_test.go create mode 100644 http/api.go create mode 100644 http/api_test.go create mode 100644 interfaces/auth.go diff --git a/Gopkg.lock b/Gopkg.lock index 5736959..ba451f4 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -181,6 +181,13 @@ revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0" version = "v1.1.4" +[[projects]] + branch = "issue-18" + name = "github.com/thedevsaddam/govalidator" + packages = ["."] + revision = "59055296916bb3c6ad9cf3b21d5f2cf7059f8e76" + source = "https://github.com/erickskrauch/govalidator.git" + [[projects]] branch = "master" name = "golang.org/x/crypto" @@ -214,6 +221,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "a12e681ec671ce8a93256cd754d4e70797476b2d2ce4379c3860df09c4b6a552" + inputs-digest = "b7c6dd9fffc543dc24b5832c7767632e4c066189be7c40868ba5612f5f45dc64" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index a1ba591..1c45e9d 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -34,6 +34,15 @@ ignored = ["elyby/minecraft-skinsystem"] name = "github.com/segmentio/go-prompt" branch = "master" +[[constraint]] + name = "github.com/thedevsaddam/govalidator" + source = "https://github.com/erickskrauch/govalidator.git" + branch = "issue-18" + +[[constraint]] + branch = "master" + name = "github.com/spf13/afero" + # Testing dependencies [[constraint]] diff --git a/auth/jwt.go b/auth/jwt.go index 8ed9f97..fcb3365 100644 --- a/auth/jwt.go +++ b/auth/jwt.go @@ -2,17 +2,21 @@ package auth import ( "encoding/base64" - "io/ioutil" "math" "math/rand" + "net/http" "os" + "strings" "time" "github.com/SermoDigital/jose/crypto" "github.com/SermoDigital/jose/jws" "github.com/mitchellh/go-homedir" + "github.com/spf13/afero" ) +var fs = afero.NewOsFs() + var hashAlg = crypto.SigningMethodHS256 const appHomeDirName = ".minecraft-skinsystem" @@ -52,39 +56,68 @@ func (t *JwtAuth) GenerateSigningKey() error { } key := generateRandomBytes(64) - if err := ioutil.WriteFile(getKeyPath(), key, 0600); err != nil { + if err := afero.WriteFile(fs, getKeyPath(), key, 0600); err != nil { return err } return nil } -func (t *JwtAuth) getSigningKey() ([]byte, error) { - if t.signingKey != nil { - return t.signingKey, nil +func (t *JwtAuth) Check(req *http.Request) error { + bearerToken := req.Header.Get("Authorization") + if bearerToken == "" { + return &Unauthorized{"Authentication header not presented"} } - path := getKeyPath() - if _, err := os.Stat(path); err != nil { - if os.IsNotExist(err) { - return nil, &SigningKeyNotAvailable{} + if !strings.EqualFold(bearerToken[0:7], "BEARER ") { + return &Unauthorized{"Cannot recognize JWT token in passed value"} + } + + tokenStr := bearerToken[7:] + token, err := jws.ParseJWT([]byte(tokenStr)) + if err != nil { + return &Unauthorized{"Cannot parse passed JWT token"} + } + + signKey, err := t.getSigningKey() + if err != nil { + return err + } + + err = token.Validate(signKey, hashAlg) + if err != nil { + return &Unauthorized{"JWT token have invalid signature. It corrupted or expired."} + } + + return nil +} + +func (t *JwtAuth) getSigningKey() ([]byte, error) { + if t.signingKey == nil { + path := getKeyPath() + if _, err := fs.Stat(path); err != nil { + if os.IsNotExist(err) { + return nil, &SigningKeyNotAvailable{} + } + + return nil, err } - return nil, err + key, err := afero.ReadFile(fs, path) + if err != nil { + return nil, err + } + + t.signingKey = key } - key, err := ioutil.ReadFile(path) - if err != nil { - return nil, err - } - - return key, nil + return t.signingKey, nil } func createAppHomeDir() error { path := getAppHomeDirPath() - if _, err := os.Stat(path); os.IsNotExist(err) { - err := os.Mkdir(path, 0755) // rwx r-x r-x + if _, err := fs.Stat(path); os.IsNotExist(err) { + err := fs.Mkdir(path, 0755) // rwx r-x r-x if err != nil { return err } @@ -107,13 +140,28 @@ func getKeyPath() string { } func generateRandomBytes(n int) []byte { - randLen := int(math.Ceil(float64(n) / 1.37)) // base64 will increase length in 1.37 times + // base64 will increase length in 1.37 times + // +1 is needed to ensure, that after base64 we will do not have any '===' characters + randLen := int(math.Ceil(float64(n) / 1.37)) + 1 randBytes := make([]byte, randLen) rand.Read(randBytes) - resBytes := make([]byte, n) + // +5 is needed to have additional buffer for the next set of XX=== characters + resBytes := make([]byte, n + 5) base64.URLEncoding.Encode(resBytes, randBytes) - return resBytes + return resBytes[:n] +} + +type Unauthorized struct { + Reason string +} + +func (e *Unauthorized) Error() string { + if e.Reason != "" { + return e.Reason + } + + return "Unauthorized" } type SigningKeyNotAvailable struct { diff --git a/auth/jwt_test.go b/auth/jwt_test.go new file mode 100644 index 0000000..5b4e701 --- /dev/null +++ b/auth/jwt_test.go @@ -0,0 +1,168 @@ +package auth + +import ( + "net/http/httptest" + "strings" + "testing" + + "github.com/spf13/afero" + + testify "github.com/stretchr/testify/assert" +) + +const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxNTE2NjU4MTkzIiwic2NvcGVzIjoic2tpbiJ9.agbBS0qdyYMBaVfTZJAZcTTRgW1Y0kZty4H3N2JHBO8" + +func TestJwtAuth_NewToken_Success(t *testing.T) { + clearFs() + assert := testify.New(t) + + fs.Mkdir(getAppHomeDirPath(), 0755) + afero.WriteFile(fs, getKeyPath(), []byte("secret"), 0600) + + jwt := &JwtAuth{} + token, err := jwt.NewToken(SkinScope) + assert.Nil(err) + assert.NotNil(token) +} + +func TestJwtAuth_NewToken_KeyNotAvailable(t *testing.T) { + clearFs() + assert := testify.New(t) + + fs = afero.NewMemMapFs() + + jwt := &JwtAuth{} + token, err := jwt.NewToken(SkinScope) + assert.IsType(&SigningKeyNotAvailable{}, err) + assert.Nil(token) +} + +func TestJwtAuth_GenerateSigningKey_KeyNotExists(t *testing.T) { + clearFs() + assert := testify.New(t) + + jwt := &JwtAuth{} + err := jwt.GenerateSigningKey() + assert.Nil(err) + if _, err := fs.Stat(getAppHomeDirPath()); err != nil { + assert.Fail("directory not created") + } + + if _, err := fs.Stat(getKeyPath()); err != nil { + assert.Fail("signing file not created") + } + + content, _ := afero.ReadFile(fs, getKeyPath()) + assert.Len(content, 64) +} + +func TestJwtAuth_GenerateSigningKey_KeyExists(t *testing.T) { + clearFs() + assert := testify.New(t) + + fs.Mkdir(getAppHomeDirPath(), 0755) + afero.WriteFile(fs, getKeyPath(), []byte("secret"), 0600) + + jwt := &JwtAuth{} + err := jwt.GenerateSigningKey() + assert.Nil(err) + if _, err := fs.Stat(getAppHomeDirPath()); err != nil { + assert.Fail("directory not created") + } + + if _, err := fs.Stat(getKeyPath()); err != nil { + assert.Fail("signing file not created") + } + + content, _ := afero.ReadFile(fs, getKeyPath()) + assert.NotEqual([]byte("secret"), content) +} + +func TestJwtAuth_Check_EmptyRequest(t *testing.T) { + clearFs() + assert := testify.New(t) + + req := httptest.NewRequest("POST", "http://localhost", nil) + jwt := &JwtAuth{} + + err := jwt.Check(req) + assert.IsType(&Unauthorized{}, err) + assert.EqualError(err, "Authentication header not presented") +} + +func TestJwtAuth_Check_NonBearer(t *testing.T) { + clearFs() + assert := testify.New(t) + + req := httptest.NewRequest("POST", "http://localhost", nil) + req.Header.Add("Authorization", "this is not jwt") + jwt := &JwtAuth{} + + err := jwt.Check(req) + assert.IsType(&Unauthorized{}, err) + assert.EqualError(err, "Cannot recognize JWT token in passed value") +} + +func TestJwtAuth_Check_BearerButNotJwt(t *testing.T) { + clearFs() + assert := testify.New(t) + + req := httptest.NewRequest("POST", "http://localhost", nil) + req.Header.Add("Authorization", "Bearer thisIs.Not.Jwt") + jwt := &JwtAuth{} + + err := jwt.Check(req) + assert.IsType(&Unauthorized{}, err) + assert.EqualError(err, "Cannot parse passed JWT token") +} + +func TestJwtAuth_Check_SecretNotAvailable(t *testing.T) { + clearFs() + assert := testify.New(t) + + req := httptest.NewRequest("POST", "http://localhost", nil) + req.Header.Add("Authorization", "Bearer " + jwt) + jwt := &JwtAuth{} + + err := jwt.Check(req) + assert.IsType(&SigningKeyNotAvailable{}, err) +} + +func TestJwtAuth_Check_SecretInvalid(t *testing.T) { + clearFs() + assert := testify.New(t) + + req := httptest.NewRequest("POST", "http://localhost", nil) + req.Header.Add("Authorization", "Bearer " + jwt) + jwt := &JwtAuth{[]byte("this is another secret")} + + err := jwt.Check(req) + assert.IsType(&Unauthorized{}, err) + assert.EqualError(err, "JWT token have invalid signature. It corrupted or expired.") +} + +func TestJwtAuth_Check_Valid(t *testing.T) { + clearFs() + assert := testify.New(t) + + req := httptest.NewRequest("POST", "http://localhost", nil) + req.Header.Add("Authorization", "Bearer " + jwt) + jwt := &JwtAuth{[]byte("secret")} + + err := jwt.Check(req) + assert.Nil(err) +} + +func TestJwtAuth_generateRandomBytes(t *testing.T) { + assert := testify.New(t) + lengthMap := []int{12, 20, 24, 30, 32, 48, 50, 64} + for _, length := range lengthMap { + bytes := generateRandomBytes(length) + assert.Len(bytes, length) + assert.False(strings.HasSuffix(string(bytes), "="), "secret key should not ends with '=' character") + } +} + +func clearFs() { + fs = afero.NewMemMapFs() +} diff --git a/cmd/serve.go b/cmd/serve.go index d67ef97..f73769a 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -4,6 +4,8 @@ import ( "fmt" "log" + "elyby/minecraft-skinsystem/auth" + "github.com/spf13/cobra" "github.com/spf13/viper" @@ -42,9 +44,10 @@ var serveCmd = &cobra.Command{ cfg := &http.Config{ ListenSpec: fmt.Sprintf("%s:%d", viper.GetString("server.host"), viper.GetInt("server.port")), - SkinsRepo: skinsRepo, - CapesRepo: capesRepo, - Logger: logger, + SkinsRepo: skinsRepo, + CapesRepo: capesRepo, + Logger: logger, + Auth: &auth.JwtAuth{}, } if err := cfg.Run(); err != nil { diff --git a/db/redis.go b/db/redis.go index 0e963a6..e3e9d70 100644 --- a/db/redis.go +++ b/db/redis.go @@ -22,9 +22,11 @@ type RedisFactory struct { Host string Port int PoolSize int - connection util.Cmder + connection *pool.Pool } +// TODO: maybe we should manually return connection to the pool? + func (f RedisFactory) CreateSkinsRepository() (interfaces.SkinsRepository, error) { connection, err := f.getConnection() if err != nil { @@ -38,7 +40,7 @@ func (f RedisFactory) CreateCapesRepository() (interfaces.CapesRepository, error panic("capes repository not supported for this storage type") } -func (f RedisFactory) getConnection() (util.Cmder, error) { +func (f RedisFactory) getConnection() (*pool.Pool, error) { if f.connection == nil { if f.Host == "" { return nil, &ParamRequired{"host"} @@ -49,7 +51,7 @@ func (f RedisFactory) getConnection() (util.Cmder, error) { } addr := fmt.Sprintf("%s:%d", f.Host, f.Port) - conn, err := createConnection(addr, f.PoolSize) + conn, err := pool.New("tcp", addr, f.PoolSize) if err != nil { return nil, err } @@ -66,7 +68,7 @@ func (f RedisFactory) getConnection() (util.Cmder, error) { } log.Println("Redis not pinged. Try to reconnect") - conn, err := createConnection(addr, f.PoolSize) + conn, err := pool.New("tcp", addr, f.PoolSize) if err != nil { log.Printf("Cannot reconnect to redis: %v\n", err) log.Printf("Waiting %d seconds to retry\n", period) @@ -82,27 +84,44 @@ func (f RedisFactory) getConnection() (util.Cmder, error) { return f.connection, nil } -func createConnection(addr string, poolSize int) (util.Cmder, error) { - if poolSize > 1 { - return pool.New("tcp", addr, poolSize) - } else { - return redis.Dial("tcp", addr) - } -} - type redisDb struct { - conn util.Cmder + conn *pool.Pool } -const accountIdToUsernameKey string = "hash:username-to-account-id" +const accountIdToUsernameKey = "hash:username-to-account-id" func (db *redisDb) FindByUsername(username string) (*model.Skin, error) { + return findByUsername(username, db.getConn()) +} + +func (db *redisDb) FindByUserId(id int) (*model.Skin, error) { + return findByUserId(id, db.getConn()) +} + +func (db *redisDb) Save(skin *model.Skin) error { + return save(skin, db.getConn()) +} + +func (db *redisDb) RemoveByUserId(id int) error { + return removeByUserId(id, db.getConn()) +} + +func (db *redisDb) RemoveByUsername(username string) error { + return removeByUsername(username, db.getConn()) +} + +func (db *redisDb) getConn() util.Cmder { + conn, _ := db.conn.Get() + return conn +} + +func findByUsername(username string, conn util.Cmder) (*model.Skin, error) { if username == "" { return nil, &SkinNotFoundError{username} } - redisKey := buildKey(username) - response := db.conn.Cmd("GET", redisKey) + redisKey := buildUsernameKey(username) + response := conn.Cmd("GET", redisKey) if response.IsType(redis.Nil) { return nil, &SkinNotFoundError{username} } @@ -128,37 +147,72 @@ func (db *redisDb) FindByUsername(username string) (*model.Skin, error) { return skin, nil } -func (db *redisDb) FindByUserId(id int) (*model.Skin, error) { - response := db.conn.Cmd("HGET", accountIdToUsernameKey, id) +func findByUserId(id int, conn util.Cmder) (*model.Skin, error) { + response := conn.Cmd("HGET", accountIdToUsernameKey, id) if response.IsType(redis.Nil) { return nil, &SkinNotFoundError{"unknown"} } username, _ := response.Str() - return db.FindByUsername(username) + return findByUsername(username, conn) } -func (db *redisDb) Save(skin *model.Skin) error { - conn := db.conn - if poolConn, isPool := conn.(*pool.Pool); isPool { - conn, _ = poolConn.Get() +func removeByUserId(id int, conn util.Cmder) error { + record, err := findByUserId(id, conn) + if err != nil { + if _, ok := err.(*SkinNotFoundError); !ok { + return err + } } conn.Cmd("MULTI") - // Если пользователь сменил ник, то мы должны удать его ключ - if skin.OldUsername != "" && skin.OldUsername != skin.Username { - conn.Cmd("DEL", buildKey(skin.OldUsername)) + conn.Cmd("HDEL", accountIdToUsernameKey, id) + if record != nil { + conn.Cmd("DEL", buildUsernameKey(record.Username)) } - // Если это новая запись или если пользователь сменил ник, то обновляем значение в хэш-таблице + conn.Cmd("EXEC") + + return nil +} + +func removeByUsername(username string, conn util.Cmder) error { + record, err := findByUsername(username, conn) + if err != nil { + if _, ok := err.(*SkinNotFoundError); !ok { + return err + } + } + + conn.Cmd("MULTI") + + conn.Cmd("DEL", buildUsernameKey(record.Username)) + if record != nil { + conn.Cmd("HDEL", accountIdToUsernameKey, record.UserId) + } + + conn.Cmd("EXEC") + + return nil +} + +func save(skin *model.Skin, conn util.Cmder) error { + conn.Cmd("MULTI") + + // If user has changed username, then we must delete his old username record + if skin.OldUsername != "" && skin.OldUsername != skin.Username { + conn.Cmd("DEL", buildUsernameKey(skin.OldUsername)) + } + + // If this is a new record or if the user has changed username, we set the value in the hash table if skin.OldUsername != "" || skin.OldUsername != skin.Username { conn.Cmd("HSET", accountIdToUsernameKey, skin.UserId, skin.Username) } str, _ := json.Marshal(skin) - conn.Cmd("SET", buildKey(skin.Username), zlibEncode(str)) + conn.Cmd("SET", buildUsernameKey(skin.Username), zlibEncode(str)) conn.Cmd("EXEC") @@ -167,11 +221,10 @@ func (db *redisDb) Save(skin *model.Skin) error { return nil } -func buildKey(username string) string { +func buildUsernameKey(username string) string { return "username:" + strings.ToLower(username) } -//noinspection GoUnusedFunction func zlibEncode(str []byte) []byte { var buff bytes.Buffer writer := zlib.NewWriter(&buff) diff --git a/http/api.go b/http/api.go new file mode 100644 index 0000000..dbc22f0 --- /dev/null +++ b/http/api.go @@ -0,0 +1,202 @@ +package http + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "regexp" + "strconv" + + "elyby/minecraft-skinsystem/auth" + "elyby/minecraft-skinsystem/db" + "elyby/minecraft-skinsystem/interfaces" + "elyby/minecraft-skinsystem/model" + + "github.com/mono83/slf/wd" + "github.com/thedevsaddam/govalidator" +) + +func init() { + govalidator.AddCustomRule("md5", func(field string, rule string, message string, value interface{}) error { + val := []byte(value.(string)) + if ok, _ := regexp.Match(`^[a-f0-9]{32}$`, val); !ok { + if message == "" { + message = fmt.Sprintf("The %s field must be a valid md5 hash", field) + } + + return errors.New(message) + } + + return nil + }) + + govalidator.AddCustomRule("skinUploadingNotAvailable", func(field string, rule string, message string, value interface{}) error { + if message == "" { + message = "Skin uploading is temporary unavailable" + } + + return errors.New(message) + }) +} + +func (cfg *Config) PostSkin(resp http.ResponseWriter, req *http.Request) { + validationErrors := validatePostSkinRequest(req) + if validationErrors != nil { + apiBadRequest(resp, validationErrors) + return + } + + identityId, _ := strconv.Atoi(req.Form.Get("identityId")) + username := req.Form.Get("username") + + record, err := findIdentity(cfg.SkinsRepo, identityId, username) + if err != nil { + cfg.Logger.Error("Error on requesting a skin from the repository: :err", wd.ErrParam(err)) + apiServerError(resp) + return + } + + skinId, _ := strconv.Atoi(req.Form.Get("skinId")) + is18, _ := strconv.ParseBool(req.Form.Get("is1_8")) + isSlim, _ := strconv.ParseBool(req.Form.Get("isSlim")) + + record.Uuid = req.Form.Get("uuid") + record.SkinId = skinId + record.Hash = req.Form.Get("hash") + record.Is1_8 = is18 + record.IsSlim = isSlim + record.Url = req.Form.Get("url") + record.MojangTextures = req.Form.Get("mojangTextures") + record.MojangSignature = req.Form.Get("mojangSignature") + + err = cfg.SkinsRepo.Save(record) + if err != nil { + cfg.Logger.Error("Unable to save record to the repository: :err", wd.ErrParam(err)) + apiServerError(resp) + return + } + + resp.WriteHeader(http.StatusCreated) +} + +func (cfg *Config) Authenticate(handler http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + err := cfg.Auth.Check(req) + if err != nil { + if _, ok := err.(*auth.Unauthorized); ok { + apiForbidden(resp, err.Error()) + } else { + cfg.Logger.Error("Unknown error on validating api request: :err", wd.ErrParam(err)) + apiServerError(resp) + } + + return + } + + handler.ServeHTTP(resp, req) + }) +} + +func validatePostSkinRequest(request *http.Request) map[string][]string { + const maxMultipartMemory int64 = 32 << 20 + const oneOfSkinOrUrlMessage = "One of url or skin should be provided, but not both" + + request.ParseMultipartForm(maxMultipartMemory) + + validationRules := govalidator.MapData{ + "identityId": {"required", "numeric", "min:1"}, + "username": {"required"}, + "uuid": {"required", "uuid"}, + "skinId": {"required", "numeric", "min:1"}, + "url": {"url"}, + "file:skin": {"ext:png", "size:24576", "mime:image/png"}, + "hash": {"md5"}, + "is1_8": {"bool"}, + "isSlim": {"bool"}, + } + + shouldAppendSkinRequiredError := false + url := request.Form.Get("url") + _, _, skinErr := request.FormFile("skin") + if (url != "" && skinErr == nil) || (url == "" && skinErr != nil) { + shouldAppendSkinRequiredError = true + } else if skinErr == nil { + validationRules["file:skin"] = append(validationRules["file:skin"], "skinUploadingNotAvailable") + } else if url != "" { + validationRules["hash"] = append(validationRules["hash"], "required") + validationRules["is1_8"] = append(validationRules["is1_8"], "required") + validationRules["isSlim"] = append(validationRules["isSlim"], "required") + } + + mojangTextures := request.Form.Get("mojangTextures") + if mojangTextures != "" { + validationRules["mojangSignature"] = []string{"required"} + } + + validator := govalidator.New(govalidator.Options{ + Request: request, + Rules: validationRules, + RequiredDefault: false, + FormSize: maxMultipartMemory, + }) + validationResults := validator.Validate() + if shouldAppendSkinRequiredError { + validationResults["url"] = append(validationResults["url"], oneOfSkinOrUrlMessage) + validationResults["skin"] = append(validationResults["skin"], oneOfSkinOrUrlMessage) + } + + if len(validationResults) != 0 { + return validationResults + } + + return nil +} + +func findIdentity(repo interfaces.SkinsRepository, identityId int, username string) (*model.Skin, error) { + var record *model.Skin + record, err := repo.FindByUserId(identityId) + if err != nil { + if _, isSkinNotFound := err.(*db.SkinNotFoundError); !isSkinNotFound { + return nil, err + } + + record, err = repo.FindByUsername(username) + if err == nil { + repo.RemoveByUsername(username) + record.UserId = identityId + } else { + record = &model.Skin{ + UserId: identityId, + Username: username, + } + } + } else if record.Username != username { + repo.RemoveByUserId(identityId) + record.Username = username + } + + return record, nil +} + +func apiBadRequest(resp http.ResponseWriter, errorsPerField map[string][]string) { + resp.WriteHeader(http.StatusBadRequest) + resp.Header().Set("Content-Type", "application/json") + result, _ := json.Marshal(map[string]interface{}{ + "errors": errorsPerField, + }) + resp.Write(result) +} + +func apiForbidden(resp http.ResponseWriter, reason string) { + resp.WriteHeader(http.StatusForbidden) + resp.Header().Set("Content-Type", "application/json") + result, _ := json.Marshal([]interface{}{ + reason, + }) + resp.Write(result) +} + +func apiServerError(resp http.ResponseWriter) { + resp.WriteHeader(http.StatusInternalServerError) +} diff --git a/http/api_test.go b/http/api_test.go new file mode 100644 index 0000000..6062596 --- /dev/null +++ b/http/api_test.go @@ -0,0 +1,337 @@ +package http + +import ( + "bytes" + "encoding/base64" + "io/ioutil" + "mime/multipart" + "net/http/httptest" + "net/url" + "testing" + + "elyby/minecraft-skinsystem/auth" + "elyby/minecraft-skinsystem/db" + + "github.com/golang/mock/gomock" + testify "github.com/stretchr/testify/assert" +) + +func TestConfig_PostSkin_Valid(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, mocks := setupMocks(ctrl) + + resultModel := createSkinModel("mock_user", false) + resultModel.SkinId = 5 + resultModel.Hash = "94a457d92a61460cb9cb5d6f29732d2a" + resultModel.Url = "http://ely.by/minecraft/skins/default.png" + resultModel.MojangTextures = "" + resultModel.MojangSignature = "" + + mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) + mocks.Skins.EXPECT().FindByUserId(1).Return(createSkinModel("mock_user", false), nil) + mocks.Skins.EXPECT().Save(resultModel).Return(nil) + + form := url.Values{ + "identityId": {"1"}, + "username": {"mock_user"}, + "uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"}, + "skinId": {"5"}, + "hash": {"94a457d92a61460cb9cb5d6f29732d2a"}, + "is1_8": {"0"}, + "isSlim": {"0"}, + "url": {"http://ely.by/minecraft/skins/default.png"}, + } + + req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", bytes.NewBufferString(form.Encode())) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + + config.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + assert.Equal(201, resp.StatusCode) + response, _ := ioutil.ReadAll(resp.Body) + assert.Empty(string(response)) +} + +func TestConfig_PostSkin_ChangedIdentityId(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, mocks := setupMocks(ctrl) + + resultModel := createSkinModel("mock_user", false) + resultModel.UserId = 2 + resultModel.SkinId = 5 + resultModel.Hash = "94a457d92a61460cb9cb5d6f29732d2a" + resultModel.Url = "http://ely.by/minecraft/skins/default.png" + resultModel.MojangTextures = "" + resultModel.MojangSignature = "" + + form := url.Values{ + "identityId": {"2"}, + "username": {"mock_user"}, + "uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"}, + "skinId": {"5"}, + "hash": {"94a457d92a61460cb9cb5d6f29732d2a"}, + "is1_8": {"0"}, + "isSlim": {"0"}, + "url": {"http://ely.by/minecraft/skins/default.png"}, + } + + req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", bytes.NewBufferString(form.Encode())) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + + mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) + mocks.Skins.EXPECT().FindByUserId(2).Return(nil, &db.SkinNotFoundError{"unknown"}) + mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) + mocks.Skins.EXPECT().RemoveByUsername("mock_user").Return(nil) + mocks.Skins.EXPECT().Save(resultModel).Return(nil) + + config.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + assert.Equal(201, resp.StatusCode) + response, _ := ioutil.ReadAll(resp.Body) + assert.Empty(string(response)) +} + +func TestConfig_PostSkin_ChangedUsername(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, mocks := setupMocks(ctrl) + + resultModel := createSkinModel("changed_username", false) + resultModel.SkinId = 5 + resultModel.Hash = "94a457d92a61460cb9cb5d6f29732d2a" + resultModel.Url = "http://ely.by/minecraft/skins/default.png" + resultModel.MojangTextures = "" + resultModel.MojangSignature = "" + + form := url.Values{ + "identityId": {"1"}, + "username": {"changed_username"}, + "uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"}, + "skinId": {"5"}, + "hash": {"94a457d92a61460cb9cb5d6f29732d2a"}, + "is1_8": {"0"}, + "isSlim": {"0"}, + "url": {"http://ely.by/minecraft/skins/default.png"}, + } + + req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", bytes.NewBufferString(form.Encode())) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + + mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) + mocks.Skins.EXPECT().FindByUserId(1).Return(createSkinModel("mock_user", false), nil) + mocks.Skins.EXPECT().RemoveByUserId(1).Return(nil) + mocks.Skins.EXPECT().Save(resultModel).Return(nil) + + config.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + assert.Equal(201, resp.StatusCode) + response, _ := ioutil.ReadAll(resp.Body) + assert.Empty(string(response)) +} + +func TestConfig_PostSkin_CompletelyNewIdentity(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, mocks := setupMocks(ctrl) + + resultModel := createSkinModel("mock_user", false) + resultModel.SkinId = 5 + resultModel.Hash = "94a457d92a61460cb9cb5d6f29732d2a" + resultModel.Url = "http://ely.by/minecraft/skins/default.png" + resultModel.MojangTextures = "" + resultModel.MojangSignature = "" + + form := url.Values{ + "identityId": {"1"}, + "username": {"mock_user"}, + "uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"}, + "skinId": {"5"}, + "hash": {"94a457d92a61460cb9cb5d6f29732d2a"}, + "is1_8": {"0"}, + "isSlim": {"0"}, + "url": {"http://ely.by/minecraft/skins/default.png"}, + } + + req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", bytes.NewBufferString(form.Encode())) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + + mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) + mocks.Skins.EXPECT().FindByUserId(1).Return(nil, &db.SkinNotFoundError{"unknown"}) + mocks.Skins.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{"mock_user"}) + mocks.Skins.EXPECT().Save(resultModel).Return(nil) + + config.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + assert.Equal(201, resp.StatusCode) + response, _ := ioutil.ReadAll(resp.Body) + assert.Empty(string(response)) +} + +func TestConfig_PostSkin_UploadSkin(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, mocks := setupMocks(ctrl) + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + part, _ := writer.CreateFormFile("skin", "char.png") + part.Write(loadSkinFile()) + + _ = writer.WriteField("identityId", "1") + _ = writer.WriteField("username", "mock_user") + _ = writer.WriteField("uuid", "0f657aa8-bfbe-415d-b700-5750090d3af3") + _ = writer.WriteField("skinId", "5") + + err := writer.Close() + if err != nil { + panic(err) + } + + req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", body) + req.Header.Add("Content-Type", writer.FormDataContentType()) + w := httptest.NewRecorder() + + mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) + + config.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + assert.Equal(400, resp.StatusCode) + response, _ := ioutil.ReadAll(resp.Body) + assert.JSONEq(`{ + "errors": { + "skin": [ + "Skin uploading is temporary unavailable" + ] + } + }`, string(response)) +} + +func TestConfig_PostSkin_RequiredFields(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, mocks := setupMocks(ctrl) + + form := url.Values{ + "hash": {"this is not md5"}, + "mojangTextures": {"someBase64EncodedString"}, + } + + req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", bytes.NewBufferString(form.Encode())) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + + mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) + + config.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + assert.Equal(400, resp.StatusCode) + response, _ := ioutil.ReadAll(resp.Body) + assert.JSONEq(`{ + "errors": { + "identityId": [ + "The identityId field is required", + "The identityId field must be numeric", + "The identityId field must be minimum 1 char" + ], + "skinId": [ + "The skinId field is required", + "The skinId field must be numeric", + "The skinId field must be minimum 1 char" + ], + "username": [ + "The username field is required" + ], + "uuid": [ + "The uuid field is required", + "The uuid field must contain valid UUID" + ], + "hash": [ + "The hash field must be a valid md5 hash" + ], + "url": [ + "One of url or skin should be provided, but not both" + ], + "skin": [ + "One of url or skin should be provided, but not both" + ], + "mojangSignature": [ + "The mojangSignature field is required" + ] + } + }`, string(response)) +} + +func TestConfig_PostSkin_Unauthorized(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, mocks := setupMocks(ctrl) + + req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", nil) + req.Header.Add("Authorization", "Bearer invalid.jwt.token") + w := httptest.NewRecorder() + + mocks.Auth.EXPECT().Check(gomock.Any()).Return(&auth.Unauthorized{"Cannot parse passed JWT token"}) + + config.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + assert.Equal(403, resp.StatusCode) + response, _ := ioutil.ReadAll(resp.Body) + assert.JSONEq(`[ + "Cannot parse passed JWT token" + ]`, string(response)) +} + +// base64 https://github.com/mathiasbynens/small/blob/0ca3c51/png-transparent.png +var OnePxPng = []byte("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==") + +func loadSkinFile() []byte { + result := make([]byte, 92) + _, err := base64.StdEncoding.Decode(result, OnePxPng) + if err != nil { + panic(err) + } + + return result +} diff --git a/http/http.go b/http/http.go index b07fdd0..e68aedf 100644 --- a/http/http.go +++ b/http/http.go @@ -22,6 +22,7 @@ type Config struct { SkinsRepo interfaces.SkinsRepository CapesRepo interfaces.CapesRepository Logger wd.Watchdog + Auth interfaces.AuthChecker } func (cfg *Config) Run() error { @@ -59,6 +60,8 @@ func (cfg *Config) CreateHandler() http.Handler { // Legacy router.HandleFunc("/skins", cfg.SkinGET).Methods("GET") router.HandleFunc("/cloaks", cfg.CapeGET).Methods("GET") + // API + router.Handle("/api/skins", cfg.Authenticate(http.HandlerFunc(cfg.PostSkin))).Methods("POST") // 404 router.NotFoundHandler = http.HandlerFunc(cfg.NotFound) diff --git a/http/http_test.go b/http/http_test.go index 3db459d..3c58763 100644 --- a/http/http_test.go +++ b/http/http_test.go @@ -25,6 +25,7 @@ func TestBuildElyUrl(t *testing.T) { type mocks struct { Skins *mock_interfaces.MockSkinsRepository Capes *mock_interfaces.MockCapesRepository + Auth *mock_interfaces.MockAuthChecker Log *mock_wd.MockWatchdog } @@ -34,15 +35,18 @@ func setupMocks(ctrl *gomock.Controller) ( ) { skinsRepo := mock_interfaces.NewMockSkinsRepository(ctrl) capesRepo := mock_interfaces.NewMockCapesRepository(ctrl) + authChecker := mock_interfaces.NewMockAuthChecker(ctrl) wd := mock_wd.NewMockWatchdog(ctrl) return &Config{ SkinsRepo: skinsRepo, CapesRepo: capesRepo, + Auth: authChecker, Logger: wd, }, &mocks{ Skins: skinsRepo, Capes: capesRepo, + Auth: authChecker, Log: wd, } } diff --git a/interfaces/auth.go b/interfaces/auth.go new file mode 100644 index 0000000..3f645f7 --- /dev/null +++ b/interfaces/auth.go @@ -0,0 +1,7 @@ +package interfaces + +import "net/http" + +type AuthChecker interface { + Check(req *http.Request) error +} diff --git a/interfaces/mock_interfaces/mock_interfaces.go b/interfaces/mock_interfaces/mock_interfaces.go index 72b1a52..78744c4 100644 --- a/interfaces/mock_interfaces/mock_interfaces.go +++ b/interfaces/mock_interfaces/mock_interfaces.go @@ -70,6 +70,30 @@ func (_mr *MockSkinsRepositoryMockRecorder) Save(arg0 interface{}) *gomock.Call return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Save", reflect.TypeOf((*MockSkinsRepository)(nil).Save), arg0) } +// RemoveByUserId mocks base method +func (_m *MockSkinsRepository) RemoveByUserId(id int) error { + ret := _m.ctrl.Call(_m, "RemoveByUserId", id) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveByUserId indicates an expected call of RemoveByUserId +func (_mr *MockSkinsRepositoryMockRecorder) RemoveByUserId(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "RemoveByUserId", reflect.TypeOf((*MockSkinsRepository)(nil).RemoveByUserId), arg0) +} + +// RemoveByUsername mocks base method +func (_m *MockSkinsRepository) RemoveByUsername(username string) error { + ret := _m.ctrl.Call(_m, "RemoveByUsername", username) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveByUsername indicates an expected call of RemoveByUsername +func (_mr *MockSkinsRepositoryMockRecorder) RemoveByUsername(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "RemoveByUsername", reflect.TypeOf((*MockSkinsRepository)(nil).RemoveByUsername), arg0) +} + // MockCapesRepository is a mock of CapesRepository interface type MockCapesRepository struct { ctrl *gomock.Controller diff --git a/interfaces/repositories.go b/interfaces/repositories.go index 94164e9..5fdca61 100644 --- a/interfaces/repositories.go +++ b/interfaces/repositories.go @@ -8,6 +8,8 @@ type SkinsRepository interface { FindByUsername(username string) (*model.Skin, error) FindByUserId(id int) (*model.Skin, error) Save(skin *model.Skin) error + RemoveByUserId(id int) error + RemoveByUsername(username string) error } type CapesRepository interface { diff --git a/script/mocks b/script/mocks index d73d1ef..4bab1ab 100755 --- a/script/mocks +++ b/script/mocks @@ -2,3 +2,4 @@ mockgen -source=interfaces/repositories.go -destination=interfaces/mock_interfaces/mock_interfaces.go mockgen -source=interfaces/api.go -destination=interfaces/mock_interfaces/mock_api.go +mockgen -source=interfaces/auth.go -destination=interfaces/mock_interfaces/mock_auth.go From 1e2f30c6c73c8e00f6634eba5ab4229cf367a4bc Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Tue, 23 Jan 2018 18:53:14 +0300 Subject: [PATCH 07/26] Forgot to commit auth checker interface mock --- interfaces/mock_interfaces/mock_auth.go | 45 +++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 interfaces/mock_interfaces/mock_auth.go diff --git a/interfaces/mock_interfaces/mock_auth.go b/interfaces/mock_interfaces/mock_auth.go new file mode 100644 index 0000000..6b78454 --- /dev/null +++ b/interfaces/mock_interfaces/mock_auth.go @@ -0,0 +1,45 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: interfaces/auth.go + +package mock_interfaces + +import ( + gomock "github.com/golang/mock/gomock" + http "net/http" + reflect "reflect" +) + +// MockAuthChecker is a mock of AuthChecker interface +type MockAuthChecker struct { + ctrl *gomock.Controller + recorder *MockAuthCheckerMockRecorder +} + +// MockAuthCheckerMockRecorder is the mock recorder for MockAuthChecker +type MockAuthCheckerMockRecorder struct { + mock *MockAuthChecker +} + +// NewMockAuthChecker creates a new mock instance +func NewMockAuthChecker(ctrl *gomock.Controller) *MockAuthChecker { + mock := &MockAuthChecker{ctrl: ctrl} + mock.recorder = &MockAuthCheckerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (_m *MockAuthChecker) EXPECT() *MockAuthCheckerMockRecorder { + return _m.recorder +} + +// Check mocks base method +func (_m *MockAuthChecker) Check(req *http.Request) error { + ret := _m.ctrl.Call(_m, "Check", req) + ret0, _ := ret[0].(error) + return ret0 +} + +// Check indicates an expected call of Check +func (_mr *MockAuthCheckerMockRecorder) Check(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Check", reflect.TypeOf((*MockAuthChecker)(nil).Check), arg0) +} From 968c83db993347cbf0abd7ecdaf13b0839c7c3b6 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Tue, 23 Jan 2018 22:58:42 +0300 Subject: [PATCH 08/26] Implemented skin deleting --- http/api.go | 43 +++++++++++++++++++ http/api_test.go | 106 +++++++++++++++++++++++++++++++++++++++++++++-- http/http.go | 2 + 3 files changed, 147 insertions(+), 4 deletions(-) diff --git a/http/api.go b/http/api.go index dbc22f0..ca9f3c5 100644 --- a/http/api.go +++ b/http/api.go @@ -13,6 +13,7 @@ import ( "elyby/minecraft-skinsystem/interfaces" "elyby/minecraft-skinsystem/model" + "github.com/gorilla/mux" "github.com/mono83/slf/wd" "github.com/thedevsaddam/govalidator" ) @@ -80,6 +81,28 @@ func (cfg *Config) PostSkin(resp http.ResponseWriter, req *http.Request) { resp.WriteHeader(http.StatusCreated) } +func (cfg *Config) DeleteSkinByUserId(resp http.ResponseWriter, req *http.Request) { + id, _ := strconv.Atoi(mux.Vars(req)["id"]) + skin, err := cfg.SkinsRepo.FindByUserId(id) + if err != nil { + apiNotFound(resp, "Cannot find record for requested user id") + return + } + + cfg.deleteSkin(skin, resp) +} + +func (cfg *Config) DeleteSkinByUsername(resp http.ResponseWriter, req *http.Request) { + username := mux.Vars(req)["username"] + skin, err := cfg.SkinsRepo.FindByUsername(username) + if err != nil { + apiNotFound(resp, "Cannot find record for requested username") + return + } + + cfg.deleteSkin(skin, resp) +} + func (cfg *Config) Authenticate(handler http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { err := cfg.Auth.Check(req) @@ -98,6 +121,17 @@ func (cfg *Config) Authenticate(handler http.Handler) http.Handler { }) } +func (cfg *Config) deleteSkin(skin *model.Skin, resp http.ResponseWriter) { + err := cfg.SkinsRepo.RemoveByUserId(skin.UserId) + if err != nil { + cfg.Logger.Error("Cannot delete skin by error: :err", wd.ErrParam(err)) + apiServerError(resp) + return + } + + resp.WriteHeader(http.StatusNoContent) +} + func validatePostSkinRequest(request *http.Request) map[string][]string { const maxMultipartMemory int64 = 32 << 20 const oneOfSkinOrUrlMessage = "One of url or skin should be provided, but not both" @@ -197,6 +231,15 @@ func apiForbidden(resp http.ResponseWriter, reason string) { resp.Write(result) } +func apiNotFound(resp http.ResponseWriter, reason string) { + resp.WriteHeader(http.StatusNotFound) + resp.Header().Set("Content-Type", "application/json") + result, _ := json.Marshal([]interface{}{ + reason, + }) + resp.Write(result) +} + func apiServerError(resp http.ResponseWriter) { resp.WriteHeader(http.StatusInternalServerError) } diff --git a/http/api_test.go b/http/api_test.go index 6062596..7d886ca 100644 --- a/http/api_test.go +++ b/http/api_test.go @@ -56,7 +56,7 @@ func TestConfig_PostSkin_Valid(t *testing.T) { defer resp.Body.Close() assert.Equal(201, resp.StatusCode) response, _ := ioutil.ReadAll(resp.Body) - assert.Empty(string(response)) + assert.Empty(response) } func TestConfig_PostSkin_ChangedIdentityId(t *testing.T) { @@ -102,7 +102,7 @@ func TestConfig_PostSkin_ChangedIdentityId(t *testing.T) { defer resp.Body.Close() assert.Equal(201, resp.StatusCode) response, _ := ioutil.ReadAll(resp.Body) - assert.Empty(string(response)) + assert.Empty(response) } func TestConfig_PostSkin_ChangedUsername(t *testing.T) { @@ -146,7 +146,7 @@ func TestConfig_PostSkin_ChangedUsername(t *testing.T) { defer resp.Body.Close() assert.Equal(201, resp.StatusCode) response, _ := ioutil.ReadAll(resp.Body) - assert.Empty(string(response)) + assert.Empty(response) } func TestConfig_PostSkin_CompletelyNewIdentity(t *testing.T) { @@ -190,7 +190,7 @@ func TestConfig_PostSkin_CompletelyNewIdentity(t *testing.T) { defer resp.Body.Close() assert.Equal(201, resp.StatusCode) response, _ := ioutil.ReadAll(resp.Body) - assert.Empty(string(response)) + assert.Empty(response) } func TestConfig_PostSkin_UploadSkin(t *testing.T) { @@ -323,6 +323,104 @@ func TestConfig_PostSkin_Unauthorized(t *testing.T) { ]`, string(response)) } +func TestConfig_DeleteSkinByUserId_Success(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, mocks := setupMocks(ctrl) + + req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/api/skins/id:1", nil) + w := httptest.NewRecorder() + + mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) + mocks.Skins.EXPECT().FindByUserId(1).Return(createSkinModel("mock_user", false), nil) + mocks.Skins.EXPECT().RemoveByUserId(1).Return(nil) + + config.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + assert.Equal(204, resp.StatusCode) + response, _ := ioutil.ReadAll(resp.Body) + assert.Empty(response) +} + +func TestConfig_DeleteSkinByUserId_NotFound(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, mocks := setupMocks(ctrl) + + req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/api/skins/id:2", nil) + w := httptest.NewRecorder() + + mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) + mocks.Skins.EXPECT().FindByUserId(2).Return(nil, &db.SkinNotFoundError{"unknown"}) + + config.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + assert.Equal(404, resp.StatusCode) + response, _ := ioutil.ReadAll(resp.Body) + assert.JSONEq(`[ + "Cannot find record for requested user id" + ]`, string(response)) +} + +func TestConfig_DeleteSkinByUsername_Success(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, mocks := setupMocks(ctrl) + + req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/api/skins/mock_user", nil) + w := httptest.NewRecorder() + + mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) + mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) + mocks.Skins.EXPECT().RemoveByUserId(1).Return(nil) + + config.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + assert.Equal(204, resp.StatusCode) + response, _ := ioutil.ReadAll(resp.Body) + assert.Empty(response) +} + +func TestConfig_DeleteSkinByUsername_NotFound(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, mocks := setupMocks(ctrl) + + req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/api/skins/mock_user_2", nil) + w := httptest.NewRecorder() + + mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) + mocks.Skins.EXPECT().FindByUsername("mock_user_2").Return(nil, &db.SkinNotFoundError{"mock_user_2"}) + + config.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + assert.Equal(404, resp.StatusCode) + response, _ := ioutil.ReadAll(resp.Body) + assert.JSONEq(`[ + "Cannot find record for requested username" + ]`, string(response)) +} + // base64 https://github.com/mathiasbynens/small/blob/0ca3c51/png-transparent.png var OnePxPng = []byte("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==") diff --git a/http/http.go b/http/http.go index e68aedf..6213166 100644 --- a/http/http.go +++ b/http/http.go @@ -62,6 +62,8 @@ func (cfg *Config) CreateHandler() http.Handler { router.HandleFunc("/cloaks", cfg.CapeGET).Methods("GET") // API router.Handle("/api/skins", cfg.Authenticate(http.HandlerFunc(cfg.PostSkin))).Methods("POST") + router.Handle("/api/skins/id:{id:[0-9]+}", cfg.Authenticate(http.HandlerFunc(cfg.DeleteSkinByUserId))).Methods("DELETE") + router.Handle("/api/skins/{username}", cfg.Authenticate(http.HandlerFunc(cfg.DeleteSkinByUsername))).Methods("DELETE") // 404 router.NotFoundHandler = http.HandlerFunc(cfg.NotFound) From f5f8fbc65e1f9a6d6bd69f6472b58351a25b2d6e Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Tue, 23 Jan 2018 23:20:28 +0300 Subject: [PATCH 09/26] Added test for the case, when signing key is not available --- http/api_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/http/api_test.go b/http/api_test.go index 7d886ca..9e979ec 100644 --- a/http/api_test.go +++ b/http/api_test.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "io/ioutil" "mime/multipart" + "net/http" "net/http/httptest" "net/url" "testing" @@ -421,6 +422,30 @@ func TestConfig_DeleteSkinByUsername_NotFound(t *testing.T) { ]`, string(response)) } +func TestConfig_Authenticate_SignatureKeyNotSet(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, mocks := setupMocks(ctrl) + + req := httptest.NewRequest("POST", "http://localhost", nil) + w := httptest.NewRecorder() + + mocks.Auth.EXPECT().Check(gomock.Any()).Return(&auth.SigningKeyNotAvailable{}) + mocks.Log.EXPECT().Error("Unknown error on validating api request: :err", gomock.Any()) + + res := config.Authenticate(http.HandlerFunc(func (resp http.ResponseWriter, req *http.Request) {})) + res.ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + assert.Equal(500, resp.StatusCode) + response, _ := ioutil.ReadAll(resp.Body) + assert.Empty(response) +} + // base64 https://github.com/mathiasbynens/small/blob/0ca3c51/png-transparent.png var OnePxPng = []byte("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==") From 855302ec603ce2067c2b36ef88376f6f6103a8a2 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Tue, 23 Jan 2018 23:49:50 +0300 Subject: [PATCH 10/26] Removed amqp worker command implementation Removed Accounts Ely.by api implementation --- Gopkg.lock | 20 +- Gopkg.toml | 4 - api/accounts/accounts.go | 166 ---------------- api/accounts/accounts_test.go | 98 ---------- api/accounts/auto-refresh-token.go | 56 ------ api/accounts/auto-refresh-token_test.go | 242 ------------------------ bootstrap/bootstrap.go | 17 -- cmd/amqpWorker.go | 67 ------- interfaces/api.go | 9 - interfaces/mock_interfaces/mock_api.go | 46 ----- script/mocks | 1 - worker/worder_test.go | 186 ------------------ worker/worker.go | 219 --------------------- 13 files changed, 1 insertion(+), 1130 deletions(-) delete mode 100644 api/accounts/accounts.go delete mode 100644 api/accounts/accounts_test.go delete mode 100644 api/accounts/auto-refresh-token.go delete mode 100644 api/accounts/auto-refresh-token_test.go delete mode 100644 cmd/amqpWorker.go delete mode 100644 interfaces/api.go delete mode 100644 interfaces/mock_interfaces/mock_api.go delete mode 100644 worker/worder_test.go delete mode 100644 worker/worker.go diff --git a/Gopkg.lock b/Gopkg.lock index ba451f4..bca5153 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -7,12 +7,6 @@ revision = "f6df55f235c24f236d11dbcf665249a59ac2021f" version = "1.1" -[[projects]] - name = "github.com/assembla/cony" - packages = ["."] - revision = "dd62697b0adb9adfda8589520cb85f4cbc2361f1" - version = "v0.3.2" - [[projects]] name = "github.com/certifi/gocertifi" packages = ["."] @@ -169,12 +163,6 @@ revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7" version = "v1.0.0" -[[projects]] - branch = "master" - name = "github.com/streadway/amqp" - packages = ["."] - revision = "2cbfe40c9341ad63ba23e53013b3ddc7989d801c" - [[projects]] name = "github.com/stretchr/testify" packages = ["assert"] @@ -206,12 +194,6 @@ packages = ["internal/gen","internal/triegen","internal/ucd","transform","unicode/cldr","unicode/norm"] revision = "bd91bbf73e9a4a801adbfb97133c992678533126" -[[projects]] - name = "gopkg.in/h2non/gock.v1" - packages = ["."] - revision = "84d599244901620fb3eb96473eb9e50619f69b47" - version = "v1.0.6" - [[projects]] branch = "v2" name = "gopkg.in/yaml.v2" @@ -221,6 +203,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "b7c6dd9fffc543dc24b5832c7767632e4c066189be7c40868ba5612f5f45dc64" + inputs-digest = "b85cbbca8b4283a0977ee92789c9beee468f2d355da5dfa28a4176934548f6f3" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 1c45e9d..8b317d5 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -19,10 +19,6 @@ ignored = ["elyby/minecraft-skinsystem"] [[constraint]] name = "github.com/getsentry/raven-go" -[[constraint]] - name = "github.com/assembla/cony" - version = "^0.3.2" - [[constraint]] name = "github.com/SermoDigital/jose" version = "~1.1.0" diff --git a/api/accounts/accounts.go b/api/accounts/accounts.go deleted file mode 100644 index a92890d..0000000 --- a/api/accounts/accounts.go +++ /dev/null @@ -1,166 +0,0 @@ -package accounts - -import ( - "encoding/json" - "fmt" - "io" - "io/ioutil" - "net/http" - "net/url" - "path" - "strings" -) - -type Config struct { - Addr string - Id string - Secret string - Scopes []string - - Client *http.Client -} - -type Token struct { - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` - ExpiresIn int `json:"expires_in"` - config *Config -} - -func (config *Config) GetToken() (*Token, error) { - form := url.Values{} - form.Add("client_id", config.Id) - form.Add("client_secret", config.Secret) - form.Add("grant_type", "client_credentials") - form.Add("scope", strings.Join(config.Scopes, ",")) - - response, err := config.getHttpClient().Post(config.getTokenUrl(), "application/x-www-form-urlencoded", strings.NewReader(form.Encode())) - if err != nil { - return nil, err - } - defer response.Body.Close() - - var result *Token - responseError := handleResponse(response) - if responseError != nil { - return nil, responseError - } - - body, _ := ioutil.ReadAll(response.Body) - unmarshalError := json.Unmarshal(body, &result) - if unmarshalError != nil { - return nil, err - } - - result.config = config - - return result, nil -} - -func (config *Config) getTokenUrl() string { - return concatenateHostAndPath(config.Addr, "/api/oauth2/v1/token") -} - -func (config *Config) getHttpClient() *http.Client { - if config.Client == nil { - config.Client = &http.Client{} - } - - return config.Client -} - -type AccountInfoResponse struct { - Id int `json:"id"` - Uuid string `json:"uuid"` - Username string `json:"username"` - Email string `json:"email"` -} - -func (token *Token) AccountInfo(attribute string, value string) (*AccountInfoResponse, error) { - request := token.newRequest("GET", token.accountInfoUrl(), nil) - - query := request.URL.Query() - query.Add(attribute, value) - request.URL.RawQuery = query.Encode() - - response, err := token.config.Client.Do(request) - if err != nil { - return nil, err - } - defer response.Body.Close() - - var info *AccountInfoResponse - - responseError := handleResponse(response) - if responseError != nil { - return nil, responseError - } - - body, _ := ioutil.ReadAll(response.Body) - json.Unmarshal(body, &info) - - return info, nil -} - -func (token *Token) accountInfoUrl() string { - return concatenateHostAndPath(token.config.Addr, "/api/internal/accounts/info") -} - -func (token *Token) newRequest(method string, urlStr string, body io.Reader) *http.Request { - request, err := http.NewRequest(method, urlStr, body) - if err != nil { - panic(err) - } - - request.Header.Add("Authorization", "Bearer " + token.AccessToken) - - return request -} - -func concatenateHostAndPath(host string, pathToJoin string) string { - u, _ := url.Parse(host) - u.Path = path.Join(u.Path, pathToJoin) - - return u.String() -} - -type UnauthorizedResponse struct {} - -func (err UnauthorizedResponse) Error() string { - return "Unauthorized response" -} - -type ForbiddenResponse struct {} - -func (err ForbiddenResponse) Error() string { - return "Forbidden response" -} - -type NotFoundResponse struct {} - -func (err NotFoundResponse) Error() string { - return "Not found" -} - -type NotSuccessResponse struct { - StatusCode int -} - -func (err NotSuccessResponse) Error() string { - return fmt.Sprintf("Response code is \"%d\"", err.StatusCode) -} - -func handleResponse(response *http.Response) error { - switch status := response.StatusCode; status { - case 200: - return nil - case 401: - return &UnauthorizedResponse{} - case 403: - return &ForbiddenResponse{} - case 404: - return &NotFoundResponse{} - default: - return &NotSuccessResponse{status} - } -} diff --git a/api/accounts/accounts_test.go b/api/accounts/accounts_test.go deleted file mode 100644 index 99be4d2..0000000 --- a/api/accounts/accounts_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package accounts - -import ( - "net/http" - "strings" - "testing" - - testify "github.com/stretchr/testify/assert" - "gopkg.in/h2non/gock.v1" -) - -func TestConfig_GetToken(t *testing.T) { - assert := testify.New(t) - - defer gock.Off() - gock.New("https://account.ely.by"). - Post("/api/oauth2/v1/token"). - Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")). - Reply(200). - JSON(map[string]interface{}{ - "access_token": "mocked-token", - "token_type": "Bearer", - "expires_in": 86400, - }) - - client := &http.Client{} - gock.InterceptClient(client) - - config := &Config{ - Addr: "https://account.ely.by", - Id: "mock-id", - Secret: "mock-secret", - Scopes: []string{"scope1", "scope2"}, - Client: client, - } - - result, err := config.GetToken() - if assert.NoError(err) { - assert.Equal("mocked-token", result.AccessToken) - assert.Equal("Bearer", result.TokenType) - assert.Equal(86400, result.ExpiresIn) - } -} - -func TestToken_AccountInfo(t *testing.T) { - assert := testify.New(t) - - defer gock.Off() - // To test valid behavior - gock.New("https://account.ely.by"). - Get("/api/internal/accounts/info"). - MatchParam("id", "1"). - MatchHeader("Authorization", "Bearer mock-token"). - Reply(200). - JSON(map[string]interface{}{ - "id": 1, - "uuid": "0f657aa8-bfbe-415d-b700-5750090d3af3", - "username": "dummy", - "email": "dummy@ely.by", - }) - - // To test behavior on invalid or expired token - gock.New("https://account.ely.by"). - Get("/api/internal/accounts/info"). - MatchParam("id", "1"). - MatchHeader("Authorization", "Bearer mock-token"). - Reply(401). - JSON(map[string]interface{}{ - "name": "Unauthorized", - "message": "Incorrect token", - "code": 0, - "status": 401, - }) - - client := &http.Client{} - gock.InterceptClient(client) - - token := &Token{ - AccessToken: "mock-token", - config: &Config{ - Addr: "https://account.ely.by", - Client: client, - }, - } - - result, err := token.AccountInfo("id", "1") - if assert.NoError(err) { - assert.Equal(1, result.Id) - assert.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", result.Uuid) - assert.Equal("dummy", result.Username) - assert.Equal("dummy@ely.by", result.Email) - } - - result2, err2 := token.AccountInfo("id", "1") - assert.Nil(result2) - assert.Error(err2) - assert.IsType(&UnauthorizedResponse{}, err2) -} diff --git a/api/accounts/auto-refresh-token.go b/api/accounts/auto-refresh-token.go deleted file mode 100644 index 9df3ba5..0000000 --- a/api/accounts/auto-refresh-token.go +++ /dev/null @@ -1,56 +0,0 @@ -package accounts - -type AutoRefresh struct { - token *Token - config *Config - repeatsCount int -} - -const repeatsLimit = 3 - -func (config *Config) GetTokenWithAutoRefresh() *AutoRefresh { - return &AutoRefresh{ - config: config, - } -} - -func (refresher *AutoRefresh) AccountInfo(attribute string, value string) (*AccountInfoResponse, error) { - defer refresher.resetRepeatsCount() - - apiToken, err := refresher.getToken() - if err != nil { - return nil, err - } - - result, err := apiToken.AccountInfo(attribute, value) - if err != nil { - _, isTokenExpire := err.(*UnauthorizedResponse) - if !isTokenExpire || refresher.repeatsCount >= repeatsLimit - 1 { - return nil, err - } - - refresher.repeatsCount++ - refresher.token = nil - - return refresher.AccountInfo(attribute, value) - } - - return result, nil -} - -func (refresher *AutoRefresh) getToken() (*Token, error) { - if refresher.token == nil { - newToken, err := refresher.config.GetToken() - if err != nil { - return nil, err - } - - refresher.token = newToken - } - - return refresher.token, nil -} - -func (refresher *AutoRefresh) resetRepeatsCount() { - refresher.repeatsCount = 0 -} diff --git a/api/accounts/auto-refresh-token_test.go b/api/accounts/auto-refresh-token_test.go deleted file mode 100644 index 4af6b88..0000000 --- a/api/accounts/auto-refresh-token_test.go +++ /dev/null @@ -1,242 +0,0 @@ -package accounts - -import ( - "net/http" - "strings" - "testing" - - testify "github.com/stretchr/testify/assert" - "gopkg.in/h2non/gock.v1" -) - -var config = &Config{ - Addr: "https://account.ely.by", - Id: "mock-id", - Secret: "mock-secret", - Scopes: []string{"scope1", "scope2"}, -} - -func TestConfig_GetTokenWithAutoRefresh(t *testing.T) { - assert := testify.New(t) - - testConfig := &Config{} - *testConfig = *config - - result := testConfig.GetTokenWithAutoRefresh() - assert.Equal(testConfig, result.config) -} - -func TestAutoRefresh_AccountInfo(t *testing.T) { - assert := testify.New(t) - - defer gock.Off() - gock.New("https://account.ely.by"). - Post("/api/oauth2/v1/token"). - Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")). - Reply(200). - JSON(map[string]interface{}{ - "access_token": "mocked-token", - "token_type": "Bearer", - "expires_in": 86400, - }) - - gock.New("https://account.ely.by"). - Get("/api/internal/accounts/info"). - Times(2). - MatchParam("id", "1"). - MatchHeader("Authorization", "Bearer mocked-token"). - Reply(200). - JSON(map[string]interface{}{ - "id": 1, - "uuid": "0f657aa8-bfbe-415d-b700-5750090d3af3", - "username": "dummy", - "email": "dummy@ely.by", - }) - - client := &http.Client{} - gock.InterceptClient(client) - - testConfig := &Config{} - *testConfig = *config - testConfig.Client = client - - autoRefresher := testConfig.GetTokenWithAutoRefresh() - result, err := autoRefresher.AccountInfo("id", "1") - if assert.NoError(err) { - assert.Equal(1, result.Id) - assert.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", result.Uuid) - assert.Equal("dummy", result.Username) - assert.Equal("dummy@ely.by", result.Email) - } - - result2, err2 := autoRefresher.AccountInfo("id", "1") - if assert.NoError(err2) { - assert.Equal(result, result2, "Results should still be same without token refreshing") - } -} - -func TestAutoRefresh_AccountInfo2(t *testing.T) { - assert := testify.New(t) - - defer gock.Off() - gock.New("https://account.ely.by"). - Post("/api/oauth2/v1/token"). - Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")). - Reply(200). - JSON(map[string]interface{}{ - "access_token": "mocked-token-1", - "token_type": "Bearer", - "expires_in": 86400, - }) - - gock.New("https://account.ely.by"). - Get("/api/internal/accounts/info"). - MatchParam("id", "1"). - MatchHeader("Authorization", "Bearer mocked-token-1"). - Reply(200). - JSON(map[string]interface{}{ - "id": 1, - "uuid": "0f657aa8-bfbe-415d-b700-5750090d3af3", - "username": "dummy", - "email": "dummy@ely.by", - }) - - gock.New("https://account.ely.by"). - Get("/api/internal/accounts/info"). - MatchParam("id", "1"). - MatchHeader("Authorization", "Bearer mocked-token-1"). - Reply(401). - JSON(map[string]interface{}{ - "name": "Unauthorized", - "message": "Incorrect token", - "code": 0, - "status": 401, - }) - - gock.New("https://account.ely.by"). - Post("/api/oauth2/v1/token"). - Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")). - Reply(200). - JSON(map[string]interface{}{ - "access_token": "mocked-token-2", - "token_type": "Bearer", - "expires_in": 86400, - }) - - gock.New("https://account.ely.by"). - Get("/api/internal/accounts/info"). - MatchParam("id", "1"). - MatchHeader("Authorization", "Bearer mocked-token-2"). - Reply(200). - JSON(map[string]interface{}{ - "id": 1, - "uuid": "0f657aa8-bfbe-415d-b700-5750090d3af3", - "username": "dummy", - "email": "dummy@ely.by", - }) - - client := &http.Client{} - gock.InterceptClient(client) - - testConfig := &Config{} - *testConfig = *config - testConfig.Client = client - - autoRefresher := testConfig.GetTokenWithAutoRefresh() - result, err := autoRefresher.AccountInfo("id", "1") - if assert.NoError(err) { - assert.Equal(1, result.Id) - assert.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", result.Uuid) - assert.Equal("dummy", result.Username) - assert.Equal("dummy@ely.by", result.Email) - } - - result2, err2 := autoRefresher.AccountInfo("id", "1") - if assert.NoError(err2) { - assert.Equal(result, result2, "Results should still be same with refreshed token") - } -} - -func TestAutoRefresh_AccountInfo3(t *testing.T) { - assert := testify.New(t) - - defer gock.Off() - gock.New("https://account.ely.by"). - Post("/api/oauth2/v1/token"). - Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")). - Reply(200). - JSON(map[string]interface{}{ - "access_token": "mocked-token-1", - "token_type": "Bearer", - "expires_in": 86400, - }) - - gock.New("https://account.ely.by"). - Get("/api/internal/accounts/info"). - MatchParam("id", "1"). - MatchHeader("Authorization", "Bearer mocked-token-1"). - Reply(404). - JSON(map[string]interface{}{ - "name": "Not Found", - "message": "Page not found.", - "code": 0, - "status": 404, - }) - - client := &http.Client{} - gock.InterceptClient(client) - - testConfig := &Config{} - *testConfig = *config - testConfig.Client = client - - autoRefresher := testConfig.GetTokenWithAutoRefresh() - result, err := autoRefresher.AccountInfo("id", "1") - assert.Nil(result) - assert.Error(err) - assert.IsType(&NotFoundResponse{}, err) -} - -func TestAutoRefresh_AccountInfo4(t *testing.T) { - assert := testify.New(t) - - defer gock.Off() - gock.New("https://account.ely.by"). - Post("/api/oauth2/v1/token"). - Times(3). - Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")). - Reply(200). - JSON(map[string]interface{}{ - "access_token": "mocked-token-1", - "token_type": "Bearer", - "expires_in": 86400, - }) - - gock.New("https://account.ely.by"). - Get("/api/internal/accounts/info"). - Times(3). - MatchParam("id", "1"). - MatchHeader("Authorization", "Bearer mocked-token-1"). - Reply(401). - JSON(map[string]interface{}{ - "name": "Unauthorized", - "message": "Incorrect token", - "code": 0, - "status": 401, - }) - - client := &http.Client{} - gock.InterceptClient(client) - - testConfig := &Config{} - *testConfig = *config - testConfig.Client = client - - autoRefresher := testConfig.GetTokenWithAutoRefresh() - result, err := autoRefresher.AccountInfo("id", "1") - assert.Nil(result) - assert.Error(err) - if !assert.IsType(&UnauthorizedResponse{}, err) { - t.Fatal(err) - } -} diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 2dc9a15..ad86505 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -1,11 +1,8 @@ package bootstrap import ( - "fmt" - "net/url" "os" - "github.com/assembla/cony" "github.com/getsentry/raven-go" "github.com/mono83/slf/rays" "github.com/mono83/slf/recievers/sentry" @@ -74,17 +71,3 @@ type RabbitMQConfig struct { Vhost string } -func CreateRabbitMQClient(config *RabbitMQConfig) *cony.Client { - addr := fmt.Sprintf( - "amqp://%s:%s@%s:%d/%s", - config.Username, - config.Password, - config.Host, - config.Port, - url.PathEscape(config.Vhost), - ) - - client := cony.NewClient(cony.URL(addr), cony.Backoff(cony.DefaultBackoff)) - - return client -} diff --git a/cmd/amqpWorker.go b/cmd/amqpWorker.go deleted file mode 100644 index ab42f69..0000000 --- a/cmd/amqpWorker.go +++ /dev/null @@ -1,67 +0,0 @@ -package cmd - -import ( - "fmt" - "log" - - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "elyby/minecraft-skinsystem/api/accounts" - "elyby/minecraft-skinsystem/bootstrap" - "elyby/minecraft-skinsystem/db" - "elyby/minecraft-skinsystem/worker" -) - -var amqpWorkerCmd = &cobra.Command{ - Use: "amqp-worker", - Short: "Launches a worker which listens to events and processes them", - Run: func(cmd *cobra.Command, args []string) { - logger, err := bootstrap.CreateLogger(viper.GetString("statsd.addr"), viper.GetString("sentry.dsn")) - if err != nil { - log.Fatal(fmt.Printf("Cannot initialize logger: %v", err)) - } - logger.Info("Logger successfully initialized") - - storageFactory := db.StorageFactory{Config: viper.GetViper()} - - logger.Info("Initializing skins repository") - skinsRepo, err := storageFactory.CreateFactory("redis").CreateSkinsRepository() - if err != nil { - logger.Emergency(fmt.Sprintf("Error on creating skins repo: %+v", err)) - return - } - logger.Info("Skins repository successfully initialized") - - logger.Info("Creating AMQP client") - amqpClient := bootstrap.CreateRabbitMQClient(&bootstrap.RabbitMQConfig{ - Host: viper.GetString("amqp.host"), - Port: viper.GetInt("amqp.port"), - Username: viper.GetString("amqp.username"), - Password: viper.GetString("amqp.password"), - Vhost: viper.GetString("amqp.vhost"), - }) - - accountsApi := (&accounts.Config{ - Addr: viper.GetString("api.accounts.host"), - Id: viper.GetString("api.accounts.id"), - Secret: viper.GetString("api.accounts.secret"), - Scopes: viper.GetStringSlice("api.accounts.scopes"), - }).GetTokenWithAutoRefresh() - - services := &worker.Services{ - Logger: logger, - AmqpClient: amqpClient, - SkinsRepo: skinsRepo, - AccountsAPI: accountsApi, - } - - if err := services.Run(); err != nil { - logger.Error(fmt.Sprintf("Cannot initialize worker: %+v", err)) - } - }, -} - -func init() { - RootCmd.AddCommand(amqpWorkerCmd) -} diff --git a/interfaces/api.go b/interfaces/api.go deleted file mode 100644 index 7b061ec..0000000 --- a/interfaces/api.go +++ /dev/null @@ -1,9 +0,0 @@ -package interfaces - -import ( - "elyby/minecraft-skinsystem/api/accounts" -) - -type AccountsAPI interface { - AccountInfo(attribute string, value string) (*accounts.AccountInfoResponse, error) -} diff --git a/interfaces/mock_interfaces/mock_api.go b/interfaces/mock_interfaces/mock_api.go deleted file mode 100644 index 8339001..0000000 --- a/interfaces/mock_interfaces/mock_api.go +++ /dev/null @@ -1,46 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: interfaces/api.go - -package mock_interfaces - -import ( - accounts "elyby/minecraft-skinsystem/api/accounts" - gomock "github.com/golang/mock/gomock" - reflect "reflect" -) - -// MockAccountsAPI is a mock of AccountsAPI interface -type MockAccountsAPI struct { - ctrl *gomock.Controller - recorder *MockAccountsAPIMockRecorder -} - -// MockAccountsAPIMockRecorder is the mock recorder for MockAccountsAPI -type MockAccountsAPIMockRecorder struct { - mock *MockAccountsAPI -} - -// NewMockAccountsAPI creates a new mock instance -func NewMockAccountsAPI(ctrl *gomock.Controller) *MockAccountsAPI { - mock := &MockAccountsAPI{ctrl: ctrl} - mock.recorder = &MockAccountsAPIMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (_m *MockAccountsAPI) EXPECT() *MockAccountsAPIMockRecorder { - return _m.recorder -} - -// AccountInfo mocks base method -func (_m *MockAccountsAPI) AccountInfo(attribute string, value string) (*accounts.AccountInfoResponse, error) { - ret := _m.ctrl.Call(_m, "AccountInfo", attribute, value) - ret0, _ := ret[0].(*accounts.AccountInfoResponse) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AccountInfo indicates an expected call of AccountInfo -func (_mr *MockAccountsAPIMockRecorder) AccountInfo(arg0, arg1 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "AccountInfo", reflect.TypeOf((*MockAccountsAPI)(nil).AccountInfo), arg0, arg1) -} diff --git a/script/mocks b/script/mocks index 4bab1ab..66a61ef 100755 --- a/script/mocks +++ b/script/mocks @@ -1,5 +1,4 @@ #!/bin/sh mockgen -source=interfaces/repositories.go -destination=interfaces/mock_interfaces/mock_interfaces.go -mockgen -source=interfaces/api.go -destination=interfaces/mock_interfaces/mock_api.go mockgen -source=interfaces/auth.go -destination=interfaces/mock_interfaces/mock_auth.go diff --git a/worker/worder_test.go b/worker/worder_test.go deleted file mode 100644 index 9bd9460..0000000 --- a/worker/worder_test.go +++ /dev/null @@ -1,186 +0,0 @@ -package worker - -import ( - "errors" - "testing" - - "github.com/golang/mock/gomock" - testify "github.com/stretchr/testify/assert" - - "elyby/minecraft-skinsystem/api/accounts" - "elyby/minecraft-skinsystem/db" - "elyby/minecraft-skinsystem/interfaces/mock_interfaces" - "elyby/minecraft-skinsystem/interfaces/mock_wd" - "elyby/minecraft-skinsystem/model" -) - -func TestServices_HandleChangeUsername(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - services, skinRepo, _, wd := setupMocks(ctrl) - - resultModel := createSourceModel() - resultModel.Username = "new_username" - - // Запись о скине существует, никаких осложнений - skinRepo.EXPECT().FindByUserId(1).Return(createSourceModel(), nil) - skinRepo.EXPECT().Save(resultModel) - wd.EXPECT().IncCounter("worker.change_username", int64(1)) - - assert.True(services.HandleChangeUsername(&UsernameChanged{ - AccountId: 1, - OldUsername: "mock_user", - NewUsername: "new_username", - })) - - // Событие с пустым ником, т.е это регистрация, так что нужно создать запись о скине - skinRepo.EXPECT().FindByUserId(1).Times(0) - skinRepo.EXPECT().Save(&model.Skin{UserId: 1, Username: "new_mock"}) - wd.EXPECT().IncCounter("worker.change_username", int64(1)) - wd.EXPECT().IncCounter("worker.change_username_empty_old_username", int64(1)) - - assert.True(services.HandleChangeUsername(&UsernameChanged{ - AccountId: 1, - OldUsername: "", - NewUsername: "new_mock", - })) - - // В базе системы скинов нет записи об указанном пользователе, так что её нужно восстановить - skinRepo.EXPECT().FindByUserId(1).Return(nil, &db.SkinNotFoundError{}) - skinRepo.EXPECT().Save(&model.Skin{UserId: 1, Username: "new_mock2"}) - wd.EXPECT().IncCounter("worker.change_username", int64(1)) - wd.EXPECT().IncCounter("worker.change_username_id_not_found", int64(1)) - wd.EXPECT().Info("Cannot find user id :accountId. Trying to search.", gomock.Any()) - - assert.True(services.HandleChangeUsername(&UsernameChanged{ - AccountId: 1, - OldUsername: "mock_user", - NewUsername: "new_mock2", - })) - - // Репозиторий вернул неожиданную ошибку - skinRepo.EXPECT().FindByUserId(1).Return(nil, errors.New("mock error")) - skinRepo.EXPECT().Save(&model.Skin{UserId: 1, Username: "new_mock2"}) - wd.EXPECT().IncCounter("worker.change_username", int64(1)) - wd.EXPECT().IncCounter("worker.change_username_id_not_found", int64(1)) - wd.EXPECT().Info("Cannot find user id :accountId. Trying to search.", gomock.Any()) - wd.EXPECT().Error("Unknown error when requesting a skin from the repository: :err", gomock.Any()) - - assert.True(services.HandleChangeUsername(&UsernameChanged{ - AccountId: 1, - OldUsername: "mock_user", - NewUsername: "new_mock2", - })) -} - -func TestServices_HandleSkinChanged(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - services, skinRepo, accountsAPI, wd := setupMocks(ctrl) - - event := &SkinChanged{ - AccountId: 1, - Uuid: "cdb907ce-84f4-4c38-801d-1e287dca2623", - SkinId: 2, - Hash: "f76caa016e07267a05b7daf9ebc7419c", - Is1_8: true, - IsSlim: false, - Url: "http://ely.by/minecraft/skins/69c6740d2993e5d6f6a7fc92420efc29.png", - MojangTextures: "new mocked textures base64", - MojangSignature: "new mocked signature", - } - - resultModel := createSourceModel() - resultModel.SkinId = event.SkinId - resultModel.Hash = event.Hash - resultModel.Is1_8 = event.Is1_8 - resultModel.IsSlim = event.IsSlim - resultModel.Url = event.Url - resultModel.MojangTextures = event.MojangTextures - resultModel.MojangSignature = event.MojangSignature - - // Запись о скине существует, никаких осложнений - skinRepo.EXPECT().FindByUserId(1).Return(createSourceModel(), nil) - skinRepo.EXPECT().Save(resultModel) - wd.EXPECT().IncCounter("worker.skin_changed", int64(1)) - - assert.True(services.HandleSkinChanged(event)) - - // Записи о скине не существует, она должна быть восстановлена - skinRepo.EXPECT().FindByUserId(1).Return(nil, &db.SkinNotFoundError{"mock_user"}) - skinRepo.EXPECT().Save(resultModel) - accountsAPI.EXPECT().AccountInfo("id", "1").Return(&accounts.AccountInfoResponse{ - Id: 1, - Username: "mock_user", - Uuid: "cdb907ce-84f4-4c38-801d-1e287dca2623", - Email: "mock-user@ely.by", - }, nil) - wd.EXPECT().IncCounter("worker.skin_changed", int64(1)) - wd.EXPECT().IncCounter("worker.skin_changed_id_not_found", int64(1)) - wd.EXPECT().IncCounter("worker.skin_changed_id_restored", int64(1)) - wd.EXPECT().Info("Cannot find user id :accountId. Trying to search.", gomock.Any()) - wd.EXPECT().Info("User info successfully restored.") - - assert.True(services.HandleSkinChanged(event)) - - // Записи о скине не существует, и Ely.by Accounts internal API не знает о таком пользователе - skinRepo.EXPECT().FindByUserId(1).Return(nil, &db.SkinNotFoundError{"mock_user"}) - accountsAPI.EXPECT().AccountInfo("id", "1").Return(nil, &accounts.NotFoundResponse{}) - wd.EXPECT().IncCounter("worker.skin_changed", int64(1)) - wd.EXPECT().IncCounter("worker.skin_changed_id_not_found", int64(1)) - wd.EXPECT().IncCounter("worker.skin_changed_id_not_restored", int64(1)) - wd.EXPECT().Info("Cannot find user id :accountId. Trying to search.", gomock.Any()) - wd.EXPECT().Error("Cannot restore user info for :accountId: :err", gomock.Any(), gomock.Any()) - - assert.True(services.HandleSkinChanged(event)) - - // Репозиторий скинов вернул неизвестную ошибку, и Ely.by Accounts internal API не знает о таком пользователе - skinRepo.EXPECT().FindByUserId(1).Return(nil, errors.New("mocked error")) - accountsAPI.EXPECT().AccountInfo("id", "1").Return(nil, &accounts.NotFoundResponse{}) - wd.EXPECT().IncCounter("worker.skin_changed", int64(1)) - wd.EXPECT().IncCounter("worker.skin_changed_id_not_found", int64(1)) - wd.EXPECT().IncCounter("worker.skin_changed_id_not_restored", int64(1)) - wd.EXPECT().Error("Unknown error when requesting a skin from the repository: :err", gomock.Any()) - wd.EXPECT().Info("Cannot find user id :accountId. Trying to search.", gomock.Any()) - wd.EXPECT().Error("Cannot restore user info for :accountId: :err", gomock.Any(), gomock.Any()) - - assert.True(services.HandleSkinChanged(event)) -} - -func createSourceModel() *model.Skin { - return &model.Skin{ - UserId: 1, - Uuid: "cdb907ce-84f4-4c38-801d-1e287dca2623", - Username: "mock_user", - SkinId: 1, - Url: "http://ely.by/minecraft/skins/3a345c701f473ac08c8c5b8ecb58ecf3.png", - Is1_8: false, - IsSlim: false, - Hash: "3a345c701f473ac08c8c5b8ecb58ecf3", - MojangTextures: "mocked textures base64", - MojangSignature: "mocked signature", - } -} - -func setupMocks(ctrl *gomock.Controller) ( - *Services, - *mock_interfaces.MockSkinsRepository, - *mock_interfaces.MockAccountsAPI, - *mock_wd.MockWatchdog, -) { - skinsRepo := mock_interfaces.NewMockSkinsRepository(ctrl) - accountApi := mock_interfaces.NewMockAccountsAPI(ctrl) - wd := mock_wd.NewMockWatchdog(ctrl) - - return &Services{ - SkinsRepo: skinsRepo, - AccountsAPI: accountApi, - Logger: wd, - }, skinsRepo, accountApi, wd -} diff --git a/worker/worker.go b/worker/worker.go deleted file mode 100644 index fe26136..0000000 --- a/worker/worker.go +++ /dev/null @@ -1,219 +0,0 @@ -package worker - -import ( - "encoding/json" - "strconv" - - "github.com/assembla/cony" - "github.com/mono83/slf/wd" - "github.com/streadway/amqp" - - "elyby/minecraft-skinsystem/db" - "elyby/minecraft-skinsystem/interfaces" - "elyby/minecraft-skinsystem/model" -) - -type Services struct { - AmqpClient *cony.Client - SkinsRepo interfaces.SkinsRepository - AccountsAPI interfaces.AccountsAPI - Logger wd.Watchdog -} - -type UsernameChanged struct { - AccountId int `json:"accountId"` - OldUsername string `json:"oldUsername"` - NewUsername string `json:"newUsername"` -} - -type SkinChanged struct { - AccountId int `json:"userId"` - Uuid string `json:"uuid"` - SkinId int `json:"skinId"` - Hash string `json:"hash"` - Is1_8 bool `json:"is1_8"` - IsSlim bool `json:"isSlim"` - Url string `json:"url"` - MojangTextures string `json:"mojangTextures"` - MojangSignature string `json:"mojangSignature"` -} - -const exchangeName string = "events" -const queueName string = "skinsystem-accounts-events" - -func (service *Services) Run() error { - clientErrs, consumerErrs, deliveryChannel := setupClient(service.AmqpClient) - shouldReturnError := true - - for service.AmqpClient.Loop() { - select { - case msg := <-deliveryChannel: - shouldReturnError = false - service.HandleDelivery(&msg) - case err := <-consumerErrs: - if shouldReturnError { - return err - } - - service.Logger.Error("Consume error: :err", wd.ErrParam(err)) - case err := <-clientErrs: - if shouldReturnError { - return err - } - - service.Logger.Error("Client error: :err", wd.ErrParam(err)) - } - } - - return nil -} - -func (service *Services) HandleDelivery(delivery *amqp.Delivery) { - service.Logger.Debug("Incoming message with routing key " + delivery.RoutingKey) - var result bool = true - switch delivery.RoutingKey { - case "accounts.username-changed": - var event *UsernameChanged - json.Unmarshal(delivery.Body, &event) - result = service.HandleChangeUsername(event) - case "accounts.skin-changed": - var event *SkinChanged - json.Unmarshal(delivery.Body, &event) - result = service.HandleSkinChanged(event) - default: - service.Logger.Info("Unknown delivery with routing key " + delivery.RoutingKey) - delivery.Ack(false) - return - } - - if result { - delivery.Ack(false) - } else { - delivery.Reject(true) - } -} - -func (service *Services) HandleChangeUsername(event *UsernameChanged) bool { - service.Logger.IncCounter("worker.change_username", 1) - if event.OldUsername == "" { - service.Logger.IncCounter("worker.change_username_empty_old_username", 1) - record := &model.Skin{ - UserId: event.AccountId, - Username: event.NewUsername, - } - - service.SkinsRepo.Save(record) - - return true - } - - record, err := service.SkinsRepo.FindByUserId(event.AccountId) - if err != nil { - service.Logger.Info("Cannot find user id :accountId. Trying to search.", wd.IntParam("accountId", event.AccountId)) - if _, isSkinNotFound := err.(*db.SkinNotFoundError); !isSkinNotFound { - service.Logger.Error("Unknown error when requesting a skin from the repository: :err", wd.ErrParam(err)) - } - - service.Logger.IncCounter("worker.change_username_id_not_found", 1) - record = &model.Skin{ - UserId: event.AccountId, - } - } - - record.Username = event.NewUsername - service.SkinsRepo.Save(record) - - return true -} - -// TODO: возможно стоит добавить проверку на совпадение id аккаунтов -func (service *Services) HandleSkinChanged(event *SkinChanged) bool { - service.Logger.IncCounter("worker.skin_changed", 1) - var record *model.Skin - record, err := service.SkinsRepo.FindByUserId(event.AccountId) - if err != nil { - if _, isSkinNotFound := err.(*db.SkinNotFoundError); !isSkinNotFound { - service.Logger.Error("Unknown error when requesting a skin from the repository: :err", wd.ErrParam(err)) - } - - service.Logger.IncCounter("worker.skin_changed_id_not_found", 1) - service.Logger.Info("Cannot find user id :accountId. Trying to search.", wd.IntParam("accountId", event.AccountId)) - response, err := service.AccountsAPI.AccountInfo("id", strconv.Itoa(event.AccountId)) - if err != nil { - service.Logger.IncCounter("worker.skin_changed_id_not_restored", 1) - service.Logger.Error( - "Cannot restore user info for :accountId: :err", - wd.IntParam("accountId", event.AccountId), - wd.ErrParam(err), - ) - - return true - } - - service.Logger.IncCounter("worker.skin_changed_id_restored", 1) - service.Logger.Info("User info successfully restored.") - - record = &model.Skin{ - UserId: response.Id, - Username: response.Username, - } - } - - record.Uuid = event.Uuid - record.SkinId = event.SkinId - record.Hash = event.Hash - record.Is1_8 = event.Is1_8 - record.IsSlim = event.IsSlim - record.Url = event.Url - record.MojangTextures = event.MojangTextures - record.MojangSignature = event.MojangSignature - - service.SkinsRepo.Save(record) - - return true -} - -func setupClient(client *cony.Client) (<-chan error, <-chan error, <-chan amqp.Delivery ) { - exchange := cony.Exchange{ - Name: exchangeName, - Kind: "topic", - Durable: true, - AutoDelete: false, - } - - queue := &cony.Queue{ - Name: queueName, - Durable: true, - AutoDelete: false, - Exclusive: false, - } - - usernameEventBinding := cony.Binding{ - Exchange: exchange, - Queue: queue, - Key: "accounts.username-changed", - } - - skinEventBinding := cony.Binding{ - Exchange: exchange, - Queue: queue, - Key: "accounts.skin-changed", - } - - declarations := []cony.Declaration{ - cony.DeclareExchange(exchange), - cony.DeclareQueue(queue), - cony.DeclareBinding(usernameEventBinding), - cony.DeclareBinding(skinEventBinding), - } - - client.Declare(declarations) - - consumer := cony.NewConsumer(queue, - cony.Qos(10), - cony.AutoTag(), - ) - client.Consume(consumer) - - return client.Errors(), consumer.Errors(), consumer.Deliveries() -} From ad7faf6e81da289fc8c48c8c0fafeaaf7b8b82e7 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Wed, 24 Jan 2018 00:14:31 +0300 Subject: [PATCH 11/26] Added statsd metrics logging for newly created API --- http/api.go | 11 +++++++++++ http/api_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/http/api.go b/http/api.go index ca9f3c5..37a43f6 100644 --- a/http/api.go +++ b/http/api.go @@ -42,8 +42,10 @@ func init() { } func (cfg *Config) PostSkin(resp http.ResponseWriter, req *http.Request) { + cfg.Logger.IncCounter("api.skins.post.request", 1) validationErrors := validatePostSkinRequest(req) if validationErrors != nil { + cfg.Logger.IncCounter("api.skins.post.validation_failed", 1) apiBadRequest(resp, validationErrors) return } @@ -78,13 +80,16 @@ func (cfg *Config) PostSkin(resp http.ResponseWriter, req *http.Request) { return } + cfg.Logger.IncCounter("api.skins.post.success", 1) resp.WriteHeader(http.StatusCreated) } func (cfg *Config) DeleteSkinByUserId(resp http.ResponseWriter, req *http.Request) { + cfg.Logger.IncCounter("api.skins.delete.request", 1) id, _ := strconv.Atoi(mux.Vars(req)["id"]) skin, err := cfg.SkinsRepo.FindByUserId(id) if err != nil { + cfg.Logger.IncCounter("api.skins.delete.not_found", 1) apiNotFound(resp, "Cannot find record for requested user id") return } @@ -93,9 +98,11 @@ func (cfg *Config) DeleteSkinByUserId(resp http.ResponseWriter, req *http.Reques } func (cfg *Config) DeleteSkinByUsername(resp http.ResponseWriter, req *http.Request) { + cfg.Logger.IncCounter("api.skins.delete.request", 1) username := mux.Vars(req)["username"] skin, err := cfg.SkinsRepo.FindByUsername(username) if err != nil { + cfg.Logger.IncCounter("api.skins.delete.not_found", 1) apiNotFound(resp, "Cannot find record for requested username") return } @@ -105,9 +112,11 @@ func (cfg *Config) DeleteSkinByUsername(resp http.ResponseWriter, req *http.Requ func (cfg *Config) Authenticate(handler http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + cfg.Logger.IncCounter("authentication.challenge", 1) err := cfg.Auth.Check(req) if err != nil { if _, ok := err.(*auth.Unauthorized); ok { + cfg.Logger.IncCounter("authentication.failed", 1) apiForbidden(resp, err.Error()) } else { cfg.Logger.Error("Unknown error on validating api request: :err", wd.ErrParam(err)) @@ -117,6 +126,7 @@ func (cfg *Config) Authenticate(handler http.Handler) http.Handler { return } + cfg.Logger.IncCounter("authentication.success", 1) handler.ServeHTTP(resp, req) }) } @@ -129,6 +139,7 @@ func (cfg *Config) deleteSkin(skin *model.Skin, resp http.ResponseWriter) { return } + cfg.Logger.IncCounter("api.skins.delete.success", 1) resp.WriteHeader(http.StatusNoContent) } diff --git a/http/api_test.go b/http/api_test.go index 9e979ec..a2a9fde 100644 --- a/http/api_test.go +++ b/http/api_test.go @@ -35,6 +35,10 @@ func TestConfig_PostSkin_Valid(t *testing.T) { mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) mocks.Skins.EXPECT().FindByUserId(1).Return(createSkinModel("mock_user", false), nil) mocks.Skins.EXPECT().Save(resultModel).Return(nil) + mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) + mocks.Log.EXPECT().IncCounter("authentication.success", int64(1)) + mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1)) + mocks.Log.EXPECT().IncCounter("api.skins.post.success", int64(1)) form := url.Values{ "identityId": {"1"}, @@ -96,6 +100,10 @@ func TestConfig_PostSkin_ChangedIdentityId(t *testing.T) { mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) mocks.Skins.EXPECT().RemoveByUsername("mock_user").Return(nil) mocks.Skins.EXPECT().Save(resultModel).Return(nil) + mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) + mocks.Log.EXPECT().IncCounter("authentication.success", int64(1)) + mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1)) + mocks.Log.EXPECT().IncCounter("api.skins.post.success", int64(1)) config.CreateHandler().ServeHTTP(w, req) @@ -140,6 +148,10 @@ func TestConfig_PostSkin_ChangedUsername(t *testing.T) { mocks.Skins.EXPECT().FindByUserId(1).Return(createSkinModel("mock_user", false), nil) mocks.Skins.EXPECT().RemoveByUserId(1).Return(nil) mocks.Skins.EXPECT().Save(resultModel).Return(nil) + mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) + mocks.Log.EXPECT().IncCounter("authentication.success", int64(1)) + mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1)) + mocks.Log.EXPECT().IncCounter("api.skins.post.success", int64(1)) config.CreateHandler().ServeHTTP(w, req) @@ -184,6 +196,10 @@ func TestConfig_PostSkin_CompletelyNewIdentity(t *testing.T) { mocks.Skins.EXPECT().FindByUserId(1).Return(nil, &db.SkinNotFoundError{"unknown"}) mocks.Skins.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{"mock_user"}) mocks.Skins.EXPECT().Save(resultModel).Return(nil) + mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) + mocks.Log.EXPECT().IncCounter("authentication.success", int64(1)) + mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1)) + mocks.Log.EXPECT().IncCounter("api.skins.post.success", int64(1)) config.CreateHandler().ServeHTTP(w, req) @@ -223,6 +239,10 @@ func TestConfig_PostSkin_UploadSkin(t *testing.T) { w := httptest.NewRecorder() mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) + mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) + mocks.Log.EXPECT().IncCounter("authentication.success", int64(1)) + mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1)) + mocks.Log.EXPECT().IncCounter("api.skins.post.validation_failed", int64(1)) config.CreateHandler().ServeHTTP(w, req) @@ -257,6 +277,10 @@ func TestConfig_PostSkin_RequiredFields(t *testing.T) { w := httptest.NewRecorder() mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) + mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) + mocks.Log.EXPECT().IncCounter("authentication.success", int64(1)) + mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1)) + mocks.Log.EXPECT().IncCounter("api.skins.post.validation_failed", int64(1)) config.CreateHandler().ServeHTTP(w, req) @@ -312,6 +336,8 @@ func TestConfig_PostSkin_Unauthorized(t *testing.T) { w := httptest.NewRecorder() mocks.Auth.EXPECT().Check(gomock.Any()).Return(&auth.Unauthorized{"Cannot parse passed JWT token"}) + mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) + mocks.Log.EXPECT().IncCounter("authentication.failed", int64(1)) config.CreateHandler().ServeHTTP(w, req) @@ -338,6 +364,10 @@ func TestConfig_DeleteSkinByUserId_Success(t *testing.T) { mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) mocks.Skins.EXPECT().FindByUserId(1).Return(createSkinModel("mock_user", false), nil) mocks.Skins.EXPECT().RemoveByUserId(1).Return(nil) + mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) + mocks.Log.EXPECT().IncCounter("authentication.success", int64(1)) + mocks.Log.EXPECT().IncCounter("api.skins.delete.request", int64(1)) + mocks.Log.EXPECT().IncCounter("api.skins.delete.success", int64(1)) config.CreateHandler().ServeHTTP(w, req) @@ -361,6 +391,10 @@ func TestConfig_DeleteSkinByUserId_NotFound(t *testing.T) { mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) mocks.Skins.EXPECT().FindByUserId(2).Return(nil, &db.SkinNotFoundError{"unknown"}) + mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) + mocks.Log.EXPECT().IncCounter("authentication.success", int64(1)) + mocks.Log.EXPECT().IncCounter("api.skins.delete.request", int64(1)) + mocks.Log.EXPECT().IncCounter("api.skins.delete.not_found", int64(1)) config.CreateHandler().ServeHTTP(w, req) @@ -387,6 +421,10 @@ func TestConfig_DeleteSkinByUsername_Success(t *testing.T) { mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) mocks.Skins.EXPECT().RemoveByUserId(1).Return(nil) + mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) + mocks.Log.EXPECT().IncCounter("authentication.success", int64(1)) + mocks.Log.EXPECT().IncCounter("api.skins.delete.request", int64(1)) + mocks.Log.EXPECT().IncCounter("api.skins.delete.success", int64(1)) config.CreateHandler().ServeHTTP(w, req) @@ -410,6 +448,10 @@ func TestConfig_DeleteSkinByUsername_NotFound(t *testing.T) { mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) mocks.Skins.EXPECT().FindByUsername("mock_user_2").Return(nil, &db.SkinNotFoundError{"mock_user_2"}) + mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) + mocks.Log.EXPECT().IncCounter("authentication.success", int64(1)) + mocks.Log.EXPECT().IncCounter("api.skins.delete.request", int64(1)) + mocks.Log.EXPECT().IncCounter("api.skins.delete.not_found", int64(1)) config.CreateHandler().ServeHTTP(w, req) @@ -435,6 +477,7 @@ func TestConfig_Authenticate_SignatureKeyNotSet(t *testing.T) { mocks.Auth.EXPECT().Check(gomock.Any()).Return(&auth.SigningKeyNotAvailable{}) mocks.Log.EXPECT().Error("Unknown error on validating api request: :err", gomock.Any()) + mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) res := config.Authenticate(http.HandlerFunc(func (resp http.ResponseWriter, req *http.Request) {})) res.ServeHTTP(w, req) From 9e4f805ed30e268faef87bb12d6e841d9108ab29 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Wed, 24 Jan 2018 00:15:48 +0300 Subject: [PATCH 12/26] Removed sentry logger implementation (we are already use accepter PR with this implementation) --- logger/receivers/sentry/receiver.go | 132 ---------------------------- 1 file changed, 132 deletions(-) delete mode 100644 logger/receivers/sentry/receiver.go diff --git a/logger/receivers/sentry/receiver.go b/logger/receivers/sentry/receiver.go deleted file mode 100644 index 9c0b872..0000000 --- a/logger/receivers/sentry/receiver.go +++ /dev/null @@ -1,132 +0,0 @@ -package sentry - -import ( - "fmt" - - "github.com/getsentry/raven-go" - "github.com/mono83/slf" - "github.com/mono83/slf/filters" -) - -// Config holds information for filtered receiver -type Config struct { - MinLevel string - ParamsWhiteList []string - ParamsBlackList []string -} - -// NewReceiver allows you to create a new receiver in the Sentry -// using the fastest and easiest way. -// The Config parameter can be passed as nil if you do not need additional filtration. -func NewReceiver(dsn string, cfg *Config) (slf.Receiver, error) { - client, err := raven.New(dsn) - if err != nil { - return nil, err - } - - return NewReceiverWithCustomRaven(client, cfg) -} - -// NewReceiverWithCustomRaven allows you to create a new receiver in the Sentry -// configuring raven.Client by yourself. This can be useful if you need to set -// additional parameters, such as release and environment, that will be sent -// with each Packet in the Sentry: -// -// client, err := raven.New("https://some:sentry@dsn.sentry.io/1") -// if err != nil { -// return nil, err -// } -// -// client.SetRelease("1.3.2") -// client.SetEnvironment("production") -// client.SetDefaultLoggerName("sentry-watchdog-receiver") -// -// sentryReceiver, err := sentry.NewReceiverWithCustomRaven(client, &sentry.Config{ -// MinLevel: "warn", -// }) -// -// The Config parameter allows you to add additional filtering, such as the minimum -// message level and the exclusion of private parameters. If you do not need additional -// filtering, nil can passed. -func NewReceiverWithCustomRaven(client *raven.Client, cfg *Config) (slf.Receiver, error) { - out, err := buildReceiverForClient(client) - if err != nil { - return nil, err - } - - if cfg == nil { - return out, nil - } - - // Resolving level - level, ok := slf.ParseType(cfg.MinLevel) - if !ok { - return nil, fmt.Errorf("Unknown level %s", cfg.MinLevel) - } - - if len(cfg.ParamsWhiteList) > 0 { - out.filter = slf.NewWhiteListParamsFilter(cfg.ParamsWhiteList) - } else { - out.filter = slf.NewBlackListParamsFilter(cfg.ParamsBlackList) - } - - return filters.MinLogLevel(level, out), nil -} - -func buildReceiverForClient(client *raven.Client) (*sentryLogReceiver, error) { - return &sentryLogReceiver{target: client, filter: slf.NewBlackListParamsFilter(nil)}, nil -} - -type sentryLogReceiver struct { - target *raven.Client - filter slf.ParamsFilter -} - -func (l sentryLogReceiver) Receive(p slf.Event) { - if !p.IsLog() { - return - } - - pkt := raven.NewPacket( - slf.ReplacePlaceholders(p.Content, p.Params, false), - // First 5 means, that first N elements will be skipped before actual app trace - // This is needed to exclude watchdog calls from stack trace - raven.NewStacktrace(5, 5, []string{}), - ) - - if len(p.Params) > 0 { - shownParams := l.filter(p.Params) - for _, param := range shownParams { - value := param.GetRaw() - if e, ok := value.(error); ok && e != nil { - value = e.Error() - } - - pkt.Extra[param.GetKey()] = value - } - } - - pkt.Level = convertType(p.Type) - pkt.Timestamp = raven.Timestamp(p.Time) - - l.target.Capture(pkt, map[string]string{}) -} - -func convertType(wdType byte) raven.Severity { - switch wdType { - case slf.TypeTrace: - case slf.TypeDebug: - return raven.DEBUG - case slf.TypeInfo: - return raven.INFO - case slf.TypeWarning: - return raven.WARNING - case slf.TypeError: - return raven.ERROR - case slf.TypeAlert: - case slf.TypeEmergency: - return raven.FATAL - } - - panic("Unknown wd type " + string(wdType)) -} From dcaa4c037d3915d555c50d4e9300782ec1fdd0a7 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Wed, 24 Jan 2018 00:23:23 +0300 Subject: [PATCH 13/26] Removed mentions of the AMQP worker and Accounts Ely.by internal API inside configs --- config.dist.yml | 20 -------------------- docker/config.dist.yml | 20 -------------------- docker/docker-compose.dev.yml | 17 ----------------- docker/docker-compose.prod.yml | 18 ------------------ docker/docker-entrypoint.sh | 2 +- 5 files changed, 1 insertion(+), 76 deletions(-) diff --git a/config.dist.yml b/config.dist.yml index 6270db8..ef890c7 100644 --- a/config.dist.yml +++ b/config.dist.yml @@ -4,16 +4,6 @@ server: host: localhost port: 80 -# Worker listen to AMQP events, so it should know how to connect to any -# AMQP provider (actually RabbitMQ). You should not escape any vhost -# characters, 'cause it will be done by application automatically -amqp: - host: localhost - port: 5672 - username: amqp-user - password: amqp-password - vhost: / - # Both of web or worker depends on storage. storage: # For now app require Redis and don't support any other backends to store @@ -32,16 +22,6 @@ storage: basePath: data capesDirName: capes -# Accounts Ely.by internal API will be used in cases, when by some reasons -# information about user will be unavailable in the app storage. -api: - accounts: - host: https://account.ely.by - id: app-id - secret: secret - scopes: - - internal_account_info - # StatsD can be used to collect metrics # statsd: # addr: localhost:3746 diff --git a/docker/config.dist.yml b/docker/config.dist.yml index 8045c06..2827222 100644 --- a/docker/config.dist.yml +++ b/docker/config.dist.yml @@ -4,16 +4,6 @@ server: host: # leave host empty to allow Docker publish port port: 80 -# Worker listen to AMQP events, so it should know how to connect to any -# AMQP provider (actually RabbitMQ). You should not escape any vhost -# characters, 'cause it will be done by application automatically -amqp: - host: rabbitmq - port: 5672 - username: minecraft-skinsystem-app - password: minecraft-skinsystem-app-password - vhost: / - # Both of web or worker depends on storage. storage: # For now app require Redis and don't support any other backends to store @@ -32,16 +22,6 @@ storage: basePath: /data capesDirName: capes -# Accounts Ely.by internal API will be used in cases, when by some reasons -# information about user will be unavailable in the app storage. -api: - accounts: - host: https://account.ely.by - id: app-id - secret: secret - scopes: - - internal_account_info - # StatsD can be used to collect metrics # statsd: # addr: localhost:3746 diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 8b6c29f..1533b4b 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -10,13 +10,6 @@ # docker-compose up -d # # 3. Pass to the project configuration links to this services: -# amqp: -# host: localhost -# port: 5672 -# username: ely -# password: ely -# vhost: /ely -# # storage: # redis: # host: localhost @@ -34,13 +27,3 @@ services: - "6379:6379" volumes: - ./data/redis:/data - - rabbitmq: - image: rabbitmq:3.6-management-alpine - ports: - - "5672:5672" - - "15672:15672" - environment: - RABBITMQ_DEFAULT_USER: "ely" - RABBITMQ_DEFAULT_PASS: "ely" - RABBITMQ_DEFAULT_VHOST: "/ely" diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 54b4512..a94f397 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -11,26 +11,8 @@ services: - ./data/capes:/data/capes - ./config/minecraft-skinsystem:/etc/minecraft-skinsystem - worker: - image: registry.ely.by/elyby/skinsystem:latest - restart: always - links: - - redis - - rabbitmq - command: ["amqp-worker"] - volumes: - - ./config/minecraft-skinsystem:/etc/minecraft-skinsystem - redis: image: redis:3.2-32bit # 32-bit version used to decrease memory usage restart: always volumes: - ./data/redis:/data - - rabbitmq: - image: rabbitmq:3.6-alpine - restart: always - environment: - RABBITMQ_DEFAULT_USER: minecraft-skinsystem-app - RABBITMQ_DEFAULT_PASS: minecraft-skinsystem-app-password - RABBITMQ_DEFAULT_VHOST: / diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index 54c11ea..9382f91 100755 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -8,7 +8,7 @@ if [ ! -f "$CONFIG" ]; then cp /usr/local/etc/minecraft-skinsystem/config.dist.yml "$CONFIG" fi -if [ "$1" = "serve" ] || [ "$1" = "amqp-worker" ]; then +if [ "$1" = "serve" ] || [ "$1" = "token" ] || [ "$1" = "version" ]; then set -- minecraft-skinsystem "$@" fi From caebac1753f41d5ccc3e9082fe4358133b8fe139 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Wed, 24 Jan 2018 01:39:40 +0300 Subject: [PATCH 14/26] Added version print by --version flag --- Gopkg.lock | 4 ++-- Gopkg.toml | 1 + cmd/root.go | 9 ++++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index bca5153..751a168 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -143,7 +143,7 @@ branch = "master" name = "github.com/spf13/cobra" packages = ["."] - revision = "3c0b56b677e04926dfa835a1b3f11cd4f62f076e" + revision = "0c34d16c3123764e413b9ed982ada58b1c3d53ea" [[projects]] branch = "master" @@ -203,6 +203,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "b85cbbca8b4283a0977ee92789c9beee468f2d355da5dfa28a4176934548f6f3" + inputs-digest = "85c318cc67a4e78dd3608297ae189cc70b07968ba6e0e1a04cc21b264fddf1eb" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 8b317d5..2364f3a 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -12,6 +12,7 @@ ignored = ["elyby/minecraft-skinsystem"] [[constraint]] name = "github.com/spf13/cobra" + branch = "master" [[constraint]] name = "github.com/spf13/viper" diff --git a/cmd/root.go b/cmd/root.go index d4b0df2..e78a97e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,6 +4,8 @@ import ( "fmt" "os" + "elyby/minecraft-skinsystem/bootstrap" + "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -11,8 +13,9 @@ import ( var cfgFile string var RootCmd = &cobra.Command{ - Use: "", - Short: "Nothing here", + Use: "", + Short: "Nothing here", + Version: bootstrap.GetVersion(), } // Execute adds all child commands to the root command and sets flags appropriately. @@ -24,7 +27,7 @@ func Execute() { } } -func init() { +func init() { cobra.OnInitialize(initConfig) RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.test.yaml)") } From 6192a58f63a69f16709964e9989fdc66410f492e Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Thu, 1 Feb 2018 22:58:34 +0300 Subject: [PATCH 15/26] Removed buildUrl() helper for automatically adding ely.by domain to skin. Now it's universtal --- http/face.go | 4 ++-- http/http.go | 9 --------- http/http_test.go | 6 ------ http/skin.go | 2 +- http/textures.go | 2 -- 5 files changed, 3 insertions(+), 20 deletions(-) diff --git a/http/face.go b/http/face.go index 2032f39..9578737 100644 --- a/http/face.go +++ b/http/face.go @@ -19,9 +19,9 @@ func (cfg *Config) Face(response http.ResponseWriter, request *http.Request) { hash = rec.Hash } - http.Redirect(response, request, buildElyUrl(buildFaceUrl(hash)), 301) + http.Redirect(response, request, buildFaceUrl(hash), 301) } func buildFaceUrl(hash string) string { - return "/minecraft/skin_buffer/faces/" + hash + ".png" + return "http://ely.by/minecraft/skin_buffer/faces/" + hash + ".png" } diff --git a/http/http.go b/http/http.go index 6213166..0020081 100644 --- a/http/http.go +++ b/http/http.go @@ -79,15 +79,6 @@ func parseUsername(username string) string { return username } -func buildElyUrl(route string) string { - prefix := "http://ely.by" - if !strings.HasPrefix(route, prefix) { - route = prefix + route - } - - return route -} - func waitForSignal() os.Signal { ch := make(chan os.Signal) signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) diff --git a/http/http_test.go b/http/http_test.go index 3c58763..45fdac3 100644 --- a/http/http_test.go +++ b/http/http_test.go @@ -16,12 +16,6 @@ func TestParseUsername(t *testing.T) { assert.Equal("test", parseUsername("test"), "Function should return string itself, if it not contains .png at end") } -func TestBuildElyUrl(t *testing.T) { - assert := testify.New(t) - assert.Equal("http://ely.by/route", buildElyUrl("/route"), "Function should add prefix to the provided relative url.") - assert.Equal("http://ely.by/test/route", buildElyUrl("http://ely.by/test/route"), "Function should do not add prefix to the provided prefixed url.") -} - type mocks struct { Skins *mock_interfaces.MockSkinsRepository Capes *mock_interfaces.MockCapesRepository diff --git a/http/skin.go b/http/skin.go index 0c8e0eb..49531dc 100644 --- a/http/skin.go +++ b/http/skin.go @@ -18,7 +18,7 @@ func (cfg *Config) Skin(response http.ResponseWriter, request *http.Request) { return } - http.Redirect(response, request, buildElyUrl(rec.Url), 301) + http.Redirect(response, request, rec.Url, 301) } func (cfg *Config) SkinGET(response http.ResponseWriter, request *http.Request) { diff --git a/http/textures.go b/http/textures.go index 4001b29..aa58529 100644 --- a/http/textures.go +++ b/http/textures.go @@ -46,8 +46,6 @@ func (cfg *Config) Textures(response http.ResponseWriter, request *http.Request) skin.Url = "http://skins.minecraft.net/MinecraftSkins/" + username + ".png" skin.Hash = string(buildNonElyTexturesHash(username)) - } else { - skin.Url = buildElyUrl(skin.Url) } textures := texturesResponse{ From ce99ac8cf8dd6ba83f035405970cbe3176fe8d8f Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Thu, 1 Feb 2018 23:16:35 +0300 Subject: [PATCH 16/26] Removed Ely-specific faces API --- http/face.go | 27 ------------------------ http/face_test.go | 53 ----------------------------------------------- http/http.go | 2 -- 3 files changed, 82 deletions(-) delete mode 100644 http/face.go delete mode 100644 http/face_test.go diff --git a/http/face.go b/http/face.go deleted file mode 100644 index 9578737..0000000 --- a/http/face.go +++ /dev/null @@ -1,27 +0,0 @@ -package http - -import ( - "net/http" - - "github.com/gorilla/mux" -) - -const defaultHash = "default" - -func (cfg *Config) Face(response http.ResponseWriter, request *http.Request) { - cfg.Logger.IncCounter("faces.request", 1) - username := parseUsername(mux.Vars(request)["username"]) - rec, err := cfg.SkinsRepo.FindByUsername(username) - var hash string - if err != nil || rec.SkinId == 0 { - hash = defaultHash - } else { - hash = rec.Hash - } - - http.Redirect(response, request, buildFaceUrl(hash), 301) -} - -func buildFaceUrl(hash string) string { - return "http://ely.by/minecraft/skin_buffer/faces/" + hash + ".png" -} diff --git a/http/face_test.go b/http/face_test.go deleted file mode 100644 index 8bb698d..0000000 --- a/http/face_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package http - -import ( - "net/http/httptest" - "testing" - - "github.com/golang/mock/gomock" - testify "github.com/stretchr/testify/assert" - - "elyby/minecraft-skinsystem/db" -) - -func TestConfig_Face(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) - mocks.Log.EXPECT().IncCounter("faces.request", int64(1)) - - req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/mock_user/face.png", nil) - w := httptest.NewRecorder() - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - assert.Equal(301, resp.StatusCode) - assert.Equal("http://ely.by/minecraft/skin_buffer/faces/55d2a8848764f5ff04012cdb093458bd.png", resp.Header.Get("Location")) -} - -func TestConfig_Face2(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - mocks.Skins.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{"mock_user"}) - mocks.Log.EXPECT().IncCounter("faces.request", int64(1)) - - req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/mock_user/face.png", nil) - w := httptest.NewRecorder() - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - assert.Equal(301, resp.StatusCode) - assert.Equal("http://ely.by/minecraft/skin_buffer/faces/default.png", resp.Header.Get("Location")) -} diff --git a/http/http.go b/http/http.go index 0020081..41d6662 100644 --- a/http/http.go +++ b/http/http.go @@ -55,8 +55,6 @@ func (cfg *Config) CreateHandler() http.Handler { router.HandleFunc("/cloaks/{username}", cfg.Cape).Methods("GET").Name("cloaks") router.HandleFunc("/textures/{username}", cfg.Textures).Methods("GET") router.HandleFunc("/textures/signed/{username}", cfg.SignedTextures).Methods("GET") - router.HandleFunc("/skins/{username}/face", cfg.Face).Methods("GET") - router.HandleFunc("/skins/{username}/face.png", cfg.Face).Methods("GET") // Legacy router.HandleFunc("/skins", cfg.SkinGET).Methods("GET") router.HandleFunc("/cloaks", cfg.CapeGET).Methods("GET") From a9f563274321e8b7d54ff752e7b2dca0bd248999 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Sun, 11 Feb 2018 16:57:20 +0300 Subject: [PATCH 17/26] Remove Ely.by documentation link from not found response --- http/not_found.go | 5 ++--- http/not_found_test.go | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/http/not_found.go b/http/not_found.go index 3328634..33e4705 100644 --- a/http/not_found.go +++ b/http/not_found.go @@ -5,11 +5,10 @@ import ( "net/http" ) -func (cfg *Config) NotFound(response http.ResponseWriter, request *http.Request) { +func (cfg *Config) NotFound(response http.ResponseWriter, request *http.Request) { data, _ := json.Marshal(map[string]string{ - "status": "404", + "status": "404", "message": "Not Found", - "link": "http://docs.ely.by/skin-system.html", }) response.Header().Set("Content-Type", "application/json") diff --git a/http/not_found_test.go b/http/not_found_test.go index 44c8a81..dfab394 100644 --- a/http/not_found_test.go +++ b/http/not_found_test.go @@ -22,7 +22,6 @@ func TestConfig_NotFound(t *testing.T) { response, _ := ioutil.ReadAll(resp.Body) assert.JSONEq(`{ "status": "404", - "message": "Not Found", - "link": "http://docs.ely.by/skin-system.html" + "message": "Not Found" }`, string(response)) } From 055f3ce6c000cb89c187f1d25dec876a7d9b4f5a Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Sun, 11 Feb 2018 17:03:13 +0300 Subject: [PATCH 18/26] Rename ely field into chrly --- http/signed_textures.go | 5 ++--- http/signed_textures_test.go | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/http/signed_textures.go b/http/signed_textures.go index 49950b1..158cdaa 100644 --- a/http/signed_textures.go +++ b/http/signed_textures.go @@ -11,7 +11,6 @@ import ( type signedTexturesResponse struct { Id string `json:"id"` Name string `json:"name"` - IsEly bool `json:"ely,omitempty"` Props []property `json:"properties"` } @@ -41,8 +40,8 @@ func (cfg *Config) SignedTextures(response http.ResponseWriter, request *http.Re Value: rec.MojangTextures, }, { - Name: "ely", - Value: "but why are you asking?", + Name: "chrly", + Value: "how do you tame a horse in Minecraft?", }, }, } diff --git a/http/signed_textures_test.go b/http/signed_textures_test.go index 56465df..3a3a9bf 100644 --- a/http/signed_textures_test.go +++ b/http/signed_textures_test.go @@ -41,8 +41,8 @@ func TestConfig_SignedTextures(t *testing.T) { "value": "mocked textures base64" }, { - "name": "ely", - "value": "but why are you asking?" + "name": "chrly", + "value": "how do you tame a horse in Minecraft?" } ] }`, string(response)) From 8dd6a581a9b6fc26ef0c55cbd9133d2fa7932aa4 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Wed, 14 Feb 2018 23:49:22 +0300 Subject: [PATCH 19/26] Fix commands descriptions --- cmd/root.go | 4 ++-- cmd/serve.go | 2 +- cmd/token.go | 6 +++--- cmd/version.go | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index e78a97e..8d65df6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -13,8 +13,8 @@ import ( var cfgFile string var RootCmd = &cobra.Command{ - Use: "", - Short: "Nothing here", + Use: "chrly", + Short: "Implementation of Minecraft skins system server", Version: bootstrap.GetVersion(), } diff --git a/cmd/serve.go b/cmd/serve.go index f73769a..29f2cea 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -16,7 +16,7 @@ import ( var serveCmd = &cobra.Command{ Use: "serve", - Short: "Runs the system server skins", + Short: "Starts http handler for the skins system", Run: func(cmd *cobra.Command, args []string) { logger, err := bootstrap.CreateLogger(viper.GetString("statsd.addr"), viper.GetString("sentry.dsn")) if err != nil { diff --git a/cmd/token.go b/cmd/token.go index a26f681..70c8724 100644 --- a/cmd/token.go +++ b/cmd/token.go @@ -12,12 +12,12 @@ import ( var tokenCmd = &cobra.Command{ Use: "token", - Short: "API tokens operations", + Short: "API tokens manipulation", } var createCmd = &cobra.Command{ Use: "create", - Short: "Create the new token, that allows interacting with Ely.by Skinsystem API", + Short: "Creates a new token, which allows to interact with Chrly API", Run: func(cmd *cobra.Command, args []string) { jwtAuth := &auth.JwtAuth{} for { @@ -43,7 +43,7 @@ var createCmd = &cobra.Command{ var resetCmd = &cobra.Command{ Use: "reset", - Short: "Regenerate the secret key, that invalidate all tokens", + Short: "Re-creates the secret key, which invalidate all tokens", Run: func(cmd *cobra.Command, args []string) { if !prompt.Confirm("Do you really want to invalidate all exists tokens?") { fmt.Println("Aboart.") diff --git a/cmd/version.go b/cmd/version.go index cd08f82..6b50b39 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -10,7 +10,7 @@ import ( var versionCmd = &cobra.Command{ Use: "version", - Short: "Show the Minecraft Skinsystem version information", + Short: "Show the Chrly version information", Run: func(cmd *cobra.Command, args []string) { fmt.Printf("Version: %s\n", bootstrap.GetVersion()) fmt.Printf("Go version: %s\n", runtime.Version()) From 235f65f11c01eec5a5e589eeccc3744e124e7670 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Thu, 15 Feb 2018 01:03:40 +0300 Subject: [PATCH 20/26] Add LICENSE --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d8eee38 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018 Ely.by (http://ely.by) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From 778bc615aa89e78e3fc494664586c2fa2493f9c9 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Thu, 15 Feb 2018 14:20:17 +0300 Subject: [PATCH 21/26] The configuration file was deleted in favor of using environment variables. Token generation functionality remove. Secret token now provided via CHRLY_SECRET env variable. --- Gopkg.lock | 28 +---------- Gopkg.toml | 11 ---- auth/jwt.go | 112 ++++------------------------------------- auth/jwt_test.go | 85 +++---------------------------- bootstrap/bootstrap.go | 9 ---- cmd/root.go | 19 ++----- cmd/serve.go | 9 +++- cmd/token.go | 48 +++--------------- http/api.go | 4 +- http/api_test.go | 16 +++--- 10 files changed, 48 insertions(+), 293 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 751a168..264c1ed 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -55,12 +55,6 @@ packages = [".","hcl/ast","hcl/parser","hcl/scanner","hcl/strconv","hcl/token","json/parser","json/scanner","json/token"] revision = "8f6b1344a92ff8877cf24a5de9177bf7d0a2a187" -[[projects]] - branch = "master" - name = "github.com/howeyc/gopass" - packages = ["."] - revision = "bf9dde6d0d2c004a008c27aaee91170c786f6db8" - [[projects]] name = "github.com/inconshreveable/mousetrap" packages = ["."] @@ -79,12 +73,6 @@ packages = ["cluster","pool","redis","util"] revision = "d234cfb904a91daafa4e1f92599a893b349cc0c2" -[[projects]] - branch = "master" - name = "github.com/mitchellh/go-homedir" - packages = ["."] - revision = "b8bc1bf767474819792c23f32d8286a45736f1c6" - [[projects]] branch = "master" name = "github.com/mitchellh/mapstructure" @@ -121,12 +109,6 @@ revision = "792786c7400a136282c1664665ae0a8db921c6c2" version = "v1.0.0" -[[projects]] - branch = "master" - name = "github.com/segmentio/go-prompt" - packages = ["."] - revision = "f0d19b6901ade831d5a3204edc0d6a7d6457fbb2" - [[projects]] branch = "master" name = "github.com/spf13/afero" @@ -176,16 +158,10 @@ revision = "59055296916bb3c6ad9cf3b21d5f2cf7059f8e76" source = "https://github.com/erickskrauch/govalidator.git" -[[projects]] - branch = "master" - name = "golang.org/x/crypto" - packages = ["ssh/terminal"] - revision = "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8" - [[projects]] branch = "master" name = "golang.org/x/sys" - packages = ["unix","windows"] + packages = ["unix"] revision = "7ddbeae9ae08c6a06a59597f0c9edbc5ff2444ce" [[projects]] @@ -203,6 +179,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "85c318cc67a4e78dd3608297ae189cc70b07968ba6e0e1a04cc21b264fddf1eb" + inputs-digest = "e6bd87f630333e3e5b03bea33720c3281a9094551bd5ced436062157fe51ab71" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 2364f3a..9e6ac7d 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -24,22 +24,11 @@ ignored = ["elyby/minecraft-skinsystem"] name = "github.com/SermoDigital/jose" version = "~1.1.0" -[[constraint]] - name = "github.com/mitchellh/go-homedir" - -[[constraint]] - name = "github.com/segmentio/go-prompt" - branch = "master" - [[constraint]] name = "github.com/thedevsaddam/govalidator" source = "https://github.com/erickskrauch/govalidator.git" branch = "issue-18" -[[constraint]] - branch = "master" - name = "github.com/spf13/afero" - # Testing dependencies [[constraint]] diff --git a/auth/jwt.go b/auth/jwt.go index fcb3365..7d86618 100644 --- a/auth/jwt.go +++ b/auth/jwt.go @@ -1,25 +1,17 @@ package auth import ( - "encoding/base64" - "math" - "math/rand" + "errors" "net/http" - "os" "strings" "time" "github.com/SermoDigital/jose/crypto" "github.com/SermoDigital/jose/jws" - "github.com/mitchellh/go-homedir" - "github.com/spf13/afero" ) -var fs = afero.NewOsFs() - var hashAlg = crypto.SigningMethodHS256 -const appHomeDirName = ".minecraft-skinsystem" const scopesClaim = "scopes" type Scope string @@ -29,20 +21,19 @@ var ( ) type JwtAuth struct { - signingKey []byte + Key []byte } func (t *JwtAuth) NewToken(scopes ...Scope) ([]byte, error) { - key, err := t.getSigningKey() - if err != nil { - return nil, err + if len(t.Key) == 0 { + return nil, errors.New("signing key not available") } claims := jws.Claims{} claims.Set(scopesClaim, scopes) claims.SetIssuedAt(time.Now()) encoder := jws.NewJWT(claims, hashAlg) - token, err := encoder.Serialize(key) + token, err := encoder.Serialize(t.Key) if err != nil { return nil, err } @@ -50,20 +41,11 @@ func (t *JwtAuth) NewToken(scopes ...Scope) ([]byte, error) { return token, nil } -func (t *JwtAuth) GenerateSigningKey() error { - if err := createAppHomeDir(); err != nil { - return err - } - - key := generateRandomBytes(64) - if err := afero.WriteFile(fs, getKeyPath(), key, 0600); err != nil { - return err - } - - return nil -} - func (t *JwtAuth) Check(req *http.Request) error { + if len(t.Key) == 0 { + return &Unauthorized{"Signing key not set"} + } + bearerToken := req.Header.Get("Authorization") if bearerToken == "" { return &Unauthorized{"Authentication header not presented"} @@ -79,79 +61,14 @@ func (t *JwtAuth) Check(req *http.Request) error { return &Unauthorized{"Cannot parse passed JWT token"} } - signKey, err := t.getSigningKey() + err = token.Validate(t.Key, hashAlg) if err != nil { - return err - } - - err = token.Validate(signKey, hashAlg) - if err != nil { - return &Unauthorized{"JWT token have invalid signature. It corrupted or expired."} + return &Unauthorized{"JWT token have invalid signature. It may be corrupted or expired."} } return nil } -func (t *JwtAuth) getSigningKey() ([]byte, error) { - if t.signingKey == nil { - path := getKeyPath() - if _, err := fs.Stat(path); err != nil { - if os.IsNotExist(err) { - return nil, &SigningKeyNotAvailable{} - } - - return nil, err - } - - key, err := afero.ReadFile(fs, path) - if err != nil { - return nil, err - } - - t.signingKey = key - } - - return t.signingKey, nil -} - -func createAppHomeDir() error { - path := getAppHomeDirPath() - if _, err := fs.Stat(path); os.IsNotExist(err) { - err := fs.Mkdir(path, 0755) // rwx r-x r-x - if err != nil { - return err - } - } - - return nil -} - -func getAppHomeDirPath() string { - path, err := homedir.Expand("~/" + appHomeDirName) - if err != nil { - panic(err) - } - - return path -} - -func getKeyPath() string { - return getAppHomeDirPath() + "/jwt-key" -} - -func generateRandomBytes(n int) []byte { - // base64 will increase length in 1.37 times - // +1 is needed to ensure, that after base64 we will do not have any '===' characters - randLen := int(math.Ceil(float64(n) / 1.37)) + 1 - randBytes := make([]byte, randLen) - rand.Read(randBytes) - // +5 is needed to have additional buffer for the next set of XX=== characters - resBytes := make([]byte, n + 5) - base64.URLEncoding.Encode(resBytes, randBytes) - - return resBytes[:n] -} - type Unauthorized struct { Reason string } @@ -163,10 +80,3 @@ func (e *Unauthorized) Error() string { return "Unauthorized" } - -type SigningKeyNotAvailable struct { -} - -func (*SigningKeyNotAvailable) Error() string { - return "Signing key not available" -} diff --git a/auth/jwt_test.go b/auth/jwt_test.go index 5b4e701..00fc65a 100644 --- a/auth/jwt_test.go +++ b/auth/jwt_test.go @@ -2,88 +2,36 @@ package auth import ( "net/http/httptest" - "strings" "testing" - "github.com/spf13/afero" - testify "github.com/stretchr/testify/assert" ) const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxNTE2NjU4MTkzIiwic2NvcGVzIjoic2tpbiJ9.agbBS0qdyYMBaVfTZJAZcTTRgW1Y0kZty4H3N2JHBO8" func TestJwtAuth_NewToken_Success(t *testing.T) { - clearFs() assert := testify.New(t) - fs.Mkdir(getAppHomeDirPath(), 0755) - afero.WriteFile(fs, getKeyPath(), []byte("secret"), 0600) - - jwt := &JwtAuth{} + jwt := &JwtAuth{[]byte("secret")} token, err := jwt.NewToken(SkinScope) assert.Nil(err) assert.NotNil(token) } func TestJwtAuth_NewToken_KeyNotAvailable(t *testing.T) { - clearFs() assert := testify.New(t) - fs = afero.NewMemMapFs() - jwt := &JwtAuth{} token, err := jwt.NewToken(SkinScope) - assert.IsType(&SigningKeyNotAvailable{}, err) + assert.Error(err, "signing key not available") assert.Nil(token) } -func TestJwtAuth_GenerateSigningKey_KeyNotExists(t *testing.T) { - clearFs() - assert := testify.New(t) - - jwt := &JwtAuth{} - err := jwt.GenerateSigningKey() - assert.Nil(err) - if _, err := fs.Stat(getAppHomeDirPath()); err != nil { - assert.Fail("directory not created") - } - - if _, err := fs.Stat(getKeyPath()); err != nil { - assert.Fail("signing file not created") - } - - content, _ := afero.ReadFile(fs, getKeyPath()) - assert.Len(content, 64) -} - -func TestJwtAuth_GenerateSigningKey_KeyExists(t *testing.T) { - clearFs() - assert := testify.New(t) - - fs.Mkdir(getAppHomeDirPath(), 0755) - afero.WriteFile(fs, getKeyPath(), []byte("secret"), 0600) - - jwt := &JwtAuth{} - err := jwt.GenerateSigningKey() - assert.Nil(err) - if _, err := fs.Stat(getAppHomeDirPath()); err != nil { - assert.Fail("directory not created") - } - - if _, err := fs.Stat(getKeyPath()); err != nil { - assert.Fail("signing file not created") - } - - content, _ := afero.ReadFile(fs, getKeyPath()) - assert.NotEqual([]byte("secret"), content) -} - func TestJwtAuth_Check_EmptyRequest(t *testing.T) { - clearFs() assert := testify.New(t) req := httptest.NewRequest("POST", "http://localhost", nil) - jwt := &JwtAuth{} + jwt := &JwtAuth{[]byte("secret")} err := jwt.Check(req) assert.IsType(&Unauthorized{}, err) @@ -91,12 +39,11 @@ func TestJwtAuth_Check_EmptyRequest(t *testing.T) { } func TestJwtAuth_Check_NonBearer(t *testing.T) { - clearFs() assert := testify.New(t) req := httptest.NewRequest("POST", "http://localhost", nil) req.Header.Add("Authorization", "this is not jwt") - jwt := &JwtAuth{} + jwt := &JwtAuth{[]byte("secret")} err := jwt.Check(req) assert.IsType(&Unauthorized{}, err) @@ -104,12 +51,11 @@ func TestJwtAuth_Check_NonBearer(t *testing.T) { } func TestJwtAuth_Check_BearerButNotJwt(t *testing.T) { - clearFs() assert := testify.New(t) req := httptest.NewRequest("POST", "http://localhost", nil) req.Header.Add("Authorization", "Bearer thisIs.Not.Jwt") - jwt := &JwtAuth{} + jwt := &JwtAuth{[]byte("secret")} err := jwt.Check(req) assert.IsType(&Unauthorized{}, err) @@ -117,7 +63,6 @@ func TestJwtAuth_Check_BearerButNotJwt(t *testing.T) { } func TestJwtAuth_Check_SecretNotAvailable(t *testing.T) { - clearFs() assert := testify.New(t) req := httptest.NewRequest("POST", "http://localhost", nil) @@ -125,11 +70,10 @@ func TestJwtAuth_Check_SecretNotAvailable(t *testing.T) { jwt := &JwtAuth{} err := jwt.Check(req) - assert.IsType(&SigningKeyNotAvailable{}, err) + assert.Error(err, "Signing key not set") } func TestJwtAuth_Check_SecretInvalid(t *testing.T) { - clearFs() assert := testify.New(t) req := httptest.NewRequest("POST", "http://localhost", nil) @@ -138,11 +82,10 @@ func TestJwtAuth_Check_SecretInvalid(t *testing.T) { err := jwt.Check(req) assert.IsType(&Unauthorized{}, err) - assert.EqualError(err, "JWT token have invalid signature. It corrupted or expired.") + assert.EqualError(err, "JWT token have invalid signature. It may be corrupted or expired.") } func TestJwtAuth_Check_Valid(t *testing.T) { - clearFs() assert := testify.New(t) req := httptest.NewRequest("POST", "http://localhost", nil) @@ -152,17 +95,3 @@ func TestJwtAuth_Check_Valid(t *testing.T) { err := jwt.Check(req) assert.Nil(err) } - -func TestJwtAuth_generateRandomBytes(t *testing.T) { - assert := testify.New(t) - lengthMap := []int{12, 20, 24, 30, 32, 48, 50, 64} - for _, length := range lengthMap { - bytes := generateRandomBytes(length) - assert.Len(bytes, length) - assert.False(strings.HasSuffix(string(bytes), "="), "secret key should not ends with '=' character") - } -} - -func clearFs() { - fs = afero.NewMemMapFs() -} diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index ad86505..d8774ee 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -62,12 +62,3 @@ func CreateLogger(statsdAddr string, sentryAddr string) (wd.Watchdog, error) { return wd.New("", "").WithParams(rays.Host), nil } - -type RabbitMQConfig struct { - Username string - Password string - Host string - Port int - Vhost string -} - diff --git a/cmd/root.go b/cmd/root.go index 8d65df6..de5fcf5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "os" + "strings" "elyby/minecraft-skinsystem/bootstrap" @@ -10,8 +11,6 @@ import ( "github.com/spf13/viper" ) -var cfgFile string - var RootCmd = &cobra.Command{ Use: "chrly", Short: "Implementation of Minecraft skins system server", @@ -29,22 +28,10 @@ func Execute() { func init() { cobra.OnInitialize(initConfig) - RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.test.yaml)") } func initConfig() { - if cfgFile != "" { - viper.SetConfigFile(cfgFile) - } else { - viper.SetConfigName("config") - viper.AddConfigPath("/etc/minecraft-skinsystem") - viper.AddConfigPath(".") - } - viper.AutomaticEnv() - - if err := viper.ReadInConfig(); err == nil { - // TODO: show only on verbose mode - fmt.Println("Using config file:", viper.ConfigFileUsed()) - } + replacer := strings.NewReplacer(".", "_") + viper.SetEnvKeyReplacer(replacer) } diff --git a/cmd/serve.go b/cmd/serve.go index 29f2cea..689afbc 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -47,7 +47,7 @@ var serveCmd = &cobra.Command{ SkinsRepo: skinsRepo, CapesRepo: capesRepo, Logger: logger, - Auth: &auth.JwtAuth{}, + Auth: &auth.JwtAuth{Key: []byte(viper.GetString("chrly.secret"))}, } if err := cfg.Run(); err != nil { @@ -58,4 +58,11 @@ var serveCmd = &cobra.Command{ func init() { RootCmd.AddCommand(serveCmd) + viper.SetDefault("server.host", "") + viper.SetDefault("server.port", 80) + viper.SetDefault("storage.redis.host", "localhost") + viper.SetDefault("storage.redis.port", 6379) + viper.SetDefault("storage.redis.poll", 10) + viper.SetDefault("storage.filesystem.basePath", "data") + viper.SetDefault("storage.filesystem.capesDirName", "capes") } diff --git a/cmd/token.go b/cmd/token.go index 70c8724..b74829e 100644 --- a/cmd/token.go +++ b/cmd/token.go @@ -6,60 +6,24 @@ import ( "elyby/minecraft-skinsystem/auth" - "github.com/segmentio/go-prompt" "github.com/spf13/cobra" + "github.com/spf13/viper" ) var tokenCmd = &cobra.Command{ Use: "token", - Short: "API tokens manipulation", -} - -var createCmd = &cobra.Command{ - Use: "create", Short: "Creates a new token, which allows to interact with Chrly API", Run: func(cmd *cobra.Command, args []string) { - jwtAuth := &auth.JwtAuth{} - for { - token, err := jwtAuth.NewToken(auth.SkinScope) - if err != nil { - if _, ok := err.(*auth.SigningKeyNotAvailable); !ok { - log.Fatalf("Unable to create new token. The error is %v\n", err) - } - - log.Println("Signing key not available. Creating...") - err := jwtAuth.GenerateSigningKey() - if err != nil { - log.Fatalf("Unable to generate new signing key. The error is %v\n", err) - } - - continue - } - - fmt.Printf("%s\n", token) - } - }, -} - -var resetCmd = &cobra.Command{ - Use: "reset", - Short: "Re-creates the secret key, which invalidate all tokens", - Run: func(cmd *cobra.Command, args []string) { - if !prompt.Confirm("Do you really want to invalidate all exists tokens?") { - fmt.Println("Aboart.") - return + jwtAuth := &auth.JwtAuth{Key: []byte(viper.GetString("chrly.secret"))} + token, err := jwtAuth.NewToken(auth.SkinScope) + if err != nil { + log.Fatalf("Unable to create new token. The error is %v\n", err) } - jwtAuth := &auth.JwtAuth{} - if err := jwtAuth.GenerateSigningKey(); err != nil { - log.Fatalf("Unable to generate new signing key. The error is %v\n", err) - } - - fmt.Println("Token successfully regenerated.") + fmt.Printf("%s\n", token) }, } func init() { - tokenCmd.AddCommand(createCmd, resetCmd) RootCmd.AddCommand(tokenCmd) } diff --git a/http/api.go b/http/api.go index 37a43f6..f81c6e3 100644 --- a/http/api.go +++ b/http/api.go @@ -236,8 +236,8 @@ func apiBadRequest(resp http.ResponseWriter, errorsPerField map[string][]string) func apiForbidden(resp http.ResponseWriter, reason string) { resp.WriteHeader(http.StatusForbidden) resp.Header().Set("Content-Type", "application/json") - result, _ := json.Marshal([]interface{}{ - reason, + result, _ := json.Marshal(map[string]interface{}{ + "error": reason, }) resp.Write(result) } diff --git a/http/api_test.go b/http/api_test.go index a2a9fde..34fc532 100644 --- a/http/api_test.go +++ b/http/api_test.go @@ -345,9 +345,9 @@ func TestConfig_PostSkin_Unauthorized(t *testing.T) { defer resp.Body.Close() assert.Equal(403, resp.StatusCode) response, _ := ioutil.ReadAll(resp.Body) - assert.JSONEq(`[ - "Cannot parse passed JWT token" - ]`, string(response)) + assert.JSONEq(`{ + "error": "Cannot parse passed JWT token" + }`, string(response)) } func TestConfig_DeleteSkinByUserId_Success(t *testing.T) { @@ -475,18 +475,20 @@ func TestConfig_Authenticate_SignatureKeyNotSet(t *testing.T) { req := httptest.NewRequest("POST", "http://localhost", nil) w := httptest.NewRecorder() - mocks.Auth.EXPECT().Check(gomock.Any()).Return(&auth.SigningKeyNotAvailable{}) - mocks.Log.EXPECT().Error("Unknown error on validating api request: :err", gomock.Any()) + mocks.Auth.EXPECT().Check(gomock.Any()).Return(&auth.Unauthorized{"signing key not available"}) mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) + mocks.Log.EXPECT().IncCounter("authentication.failed", int64(1)) res := config.Authenticate(http.HandlerFunc(func (resp http.ResponseWriter, req *http.Request) {})) res.ServeHTTP(w, req) resp := w.Result() defer resp.Body.Close() - assert.Equal(500, resp.StatusCode) + assert.Equal(403, resp.StatusCode) response, _ := ioutil.ReadAll(resp.Body) - assert.Empty(response) + assert.JSONEq(`{ + "error": "signing key not available" + }`, string(response)) } // base64 https://github.com/mathiasbynens/small/blob/0ca3c51/png-transparent.png From 55f52d0ad4cc506331f379f553efc35b7bc06335 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Thu, 15 Feb 2018 23:57:57 +0300 Subject: [PATCH 22/26] Add project README --- README.md | 288 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 233 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 675441c..878629a 100644 --- a/README.md +++ b/README.md @@ -1,74 +1,252 @@ -# Ely.by Minecraft Skinsystem +# Chrly -Реализация API системы скинов для Minecraft v4. +Chrly is a lightweight implementation of Minecraft skins system server. It's packaged and distributed as a Docker +image and can be downloaded from [Dockerhub](https://hub.docker.com/r/elyby/chrly/). App is written in Go, can +withstand heavy loads and is production ready. -## Config +## Installation -Конфигурация может задаваться посредством любого из перечисленных форматов файлов: JSON, TOML, YAML, HCL и -Java properties. Кроме того, параметры конфигурации могут перезаписываться доступными при запуске программы -ENV переменными. +You can easily install Chrly using [docker-compose](https://docs.docker.com/compose/). The configuration below (save +it as `docker-compose.yml`) can be used to start a Chrly server. It relies on `CHRLY_SECRET` environment variable +that you must set before running `docker-compose up -d`. Other possible variables are described below. -> **Заметка**: ENV переменные именуются как KEY.SUBKEY.SUBSUBKEY, т.е. все символы должны быть заглавными, - а точки должны отделять уровень вложенности. +```yml +version: '2' +services: + app: + image: elyby/chrly + hostname: chrly0 + restart: always + links: + - redis + volumes: + - ./data/capes:/data/capes + ports: + - "80:80" + environment: + CHRLY_SECRET: replace_this_value_in_production -Пример файла конфигурации находится в [config.dist.yml](config.dist.yml). Внутри dist-файла есть комментарии, -поясняющие назначение тех или иных параметров. Для работы его следует скопировать в локальный `config.yml` -и отредактировать под свои нужды. - -## Развёртывание - -Деплоить проект можно двумя способами: - -1. Скомпилировав и запустив бинарный файл, а также обеспечив ему доступ ко всем необходмым сервисам. - -2. Используя Docker и docker-compose. - -*Первый случай не буду описывать, т.к. долго, мучительно и никто так делать не будет, я гарантирую это*, -поэтому перейдём сразу ко второму. - -Прежде всего необходимо установить [Docker](https://docs.docker.com/engine/installation/) и -[docker-compose](https://docs.docker.com/compose/install/). - -Для запуска последней версии проекта достаточно скопировать содержимое файла -[docker/docker-compose.prod.yml](docker/docker-compose.prod.yml) в файл `docker-compose.yml` непосредственно -на месте установки, после чего ввести в консоль команду: - -```sh -docker-compose up -d + redis: + image: redis:4.0-32bit + restart: always + volumes: + - ./data/redis:/data ``` -Web-приложение, amqp worker и все сопутствующие сервисы будут автоматически запущены. Данные из контейнеров -будут синхронизироваться в папку `data`. +Chrly will mount some volumes on the host machine to persist storage for capes and Redis database. -## Разработка +### Config -Перво-наперво необходимо [установить последнюю версию Go](https://golang.org/doc/install) и сконфигурировать -переменную окружения GOPATH, а также установить инструмент контроля версий [dep](https://github.com/golang/dep). - -Затем можно склонировать репозиторий хитрым способом, чтобы удовлетворить все прекрасные особенности Go: +Application's configuration is based on the environment variables. You can adjust config by modifying `environment` key +inside your `docker-compose.yml` file. After value will have been changed, container should be stopped and recreated. +If environment variables have been changed, Docker will automatically recreate the container, so you only need to `stop` +and `up` it: ```sh -# Сперва создадим подпапку для приватных Go проектов Ely.by -mkdir -p $GOPATH/src/elyby -# Затем непосредственно клинируем репозиторий туда, где его ожидает увидеть Go -git clone git@gitlab.ely.by:elyby/minecraft-skinsystem.git $GOPATH/src/elyby/minecraft-skinsystem -# Переходим в папку проекта -cd $GOPATH/src/elyby/minecraft-skinsystem -# Устанавливаем зависимости +docker-compose stop app +docker-compose up -d app +``` + +**Variables to adjust:** + +| ENV | Description | Example | +|--------------------|------------------------------------------------------------------------------------|-------------------------------------------| +| STORAGE_REDIS_POOL | By default, Chrly creates pool with 10 connection, but you may want to increase it | `20` | +| STATSD_ADDR | StatsD can be used to collect metrics | `localhost:8125` | +| SENTRY_DSN | Sentry can be used to collect app errors | `https://public:private@your.sentry.io/1` | + +If something goes wrong, you can always access logs by executing `docker-compose logs -f app`. + +## Endpoints + +Each endpoint that accepts `username` as a part of an url takes it case insensitive. `.png` part can be omitted too. + +#### `GET /skins/{username}.png` + +This endpoint responds to requested `username` with a skin texture. If user's skin was set as texture's link, then it'll +respond with the `301` redirect to that url. If there is no record for requested username, it'll redirect to the +Mojang skins system as: `http://skins.minecraft.net/MinecraftSkins/{username}.png` with the original username's case. + +#### `GET /cloaks/{username}.png` + +It responds to requested `username` with a cape texture. If user's cape file doesn't exists, then it'll redirect to the +Mojang skins system as: `http://skins.minecraft.net/MinecraftCloaks/{username}.png` with the original username's case. + +#### `GET /textures/{username}` + +This endpoint forms response payloads as if it was the `textures`' property, but without base64 encoding. For example: + +```json +{ + "SKIN": { + "url": "http://ely.by/minecraft/skins/skin.png", + "hash": "55d2a8848764f5ff04012cdb093458bd", + "metadata": { + "model": "slim" + } + }, + "CAPE": { + "url": "http://skinsystem.ely.by/cloaks/username", + "hash": "424ff79dce9940af89c28ad80de8aaad" + } +} +``` + +If record for the requested username wasn't found, cape would be omitted and skin would be formed for Mojang skins +system. Hash would be formed as the username plus the half-hour-ranged time of request, which is needed to improve +caching of Mojang skins inside Minecraft. + +That request is handy in case when your server implements authentication for a game server (e.g. join/hasJoined +operation) and you have to respond with hasJoined request with an actual user textures. You have to simply send request +to the Chrly server and put the result in your hasJoined response. + +#### `GET /textures/signed/{username}` + +Actually, it's [Ely.by](http://ely.by) feature called [Server Skins System](http://ely.by/server-skins-system), but if +you have your own source of the Mojang signatures, then you can pass it with textures and it'll be displayed in this +method. Received response should be directly sent to the client without any modification via game server API. + +Response example: + +```json +{ + "id": "0f657aa8bfbe415db7005750090d3af3", + "name": "username", + "properties": [ + { + "name": "textures", + "signature": "signature value", + "value": "base64 encoded value" + }, + { + "name": "chrly", + "value": "how do you tame a horse in Minecraft?" + } + ] +} +``` + +If there is no requested `username` or `mojangSignature` field isn't set, `204` status code will be sent. + +#### `GET /skins?name={username}` + +Equivalent of the `GET /skins/{username}.png`, but constructed especially for old Minecraft versions, where username +placeholder wasn't used. + +#### `GET /cloaks?name={username}` + +Equivalent of the `GET /cloaks/{username}.png`, but constructed especially for old Minecraft versions, where username +placeholder wasn't used. + +### Records manipulating API + +Each request to the internal API should be performed with the Bearer authorization header. Example curl request: + +```sh +curl -X POST -i http://chrly.domain.com/api/skins \ + -H "Authorization: Bearer Ym9zY236Ym9zY28=" +``` + +You can obtain token by executing `docker-compose run --rm app token`. + +#### `POST /api/skins` + +> **Warning**: skin uploading via `skin` field is not implemented for now. + +Endpoint allows you to create or update skin record for a username. To upload skin, you have to send multipart +form data. `form-urlencoded` also supported, but, as you may know, it doesn't support files uploading. + +**Request params:** + +| Field | Type | Description | +|-----------------|--------|--------------------------------------------------------------------------------| +| identityId | int | Unique record identifier. | +| username | string | Username. Case insensitive. | +| uuid | uuid | UUID of the user. | +| skinId | int | Skin identifier. | +| hash | string | Skin's hash. Algorithm can be any. For example `md5`. | +| is1_8 | bool | Does the skin have the new format (64x64). | +| isSlim | bool | Does skin have slim arms (Alex model). | +| mojangTextures | string | Mojang textures field. It must be a base64 encoded json string. Not required. | +| mojangSignature | string | Signature for Mojang textures, which is required when `mojangTextures` passed. | +| url | string | Actual url of the skin. You have to pass this parameter or `skin`. | +| skin | file | Skin file. You have to pass this parameter or `url`. | + +If successful you'll receive `201` status code. In the case of failure there will be `400` status code and errors list +as json: + +```json +{ + "errors": { + "identityId": [ + "The identityId field must be numeric" + ] + } +} +``` + +#### `DELETE /api/skins/id:{identityId}` + +Performs record removal by identity id. Request body is not required. On success you will receive `204` status code. +On failure it'll be `404` with the json body: + +```json +{ + "error": "Cannot find record for requested user id" +} +``` + +#### `DELETE /api/skins/{username}` + +Same endpoint as above but it removes record by identity's username. Have the same behavior, but in case of failure +response will be: + +```json +{ + "error": "Cannot find record for requested username" +} +``` + +## Development + +First of all you should install the [latest stable version of Go](https://golang.org/doc/install) and set `GOPATH` +environment variable. + +This project uses [`dep`](https://github.com/golang/dep) for dependencies management, so it +[should be installed](https://github.com/golang/dep#installation) too. + +Then you must fork this repository. Now follow these steps: + +```sh +# Get the source code +go get github.com/elyby/chrly +# Switch to the project folder +cd $GOPATH/src/github.com/elyby/chrly +# Install dependencies (it can take a while) dep ensure +# Add your fork link as a remote +git remote add fork git@github.com:your-username/chrly.git +# Create a new branch for your task +git checkout -b iss-123 ``` -Чтобы запустить проект достаточно написать `go run main.go`, но без файла конфигурации и Redis -программа долго не проработает. Поэтому сперва копируем `config.dist.yml` в `config.yml` и, при необходимости, -затачиваем его под себя. - -Redis можно установить в систему самостоятельно, но гораздо удобнее воспользоваться готовыми сервисами, -описанными в [docker/docker-compose.dev.yml](docker/docker-compose.dev.yml). Для этого просто копируем -`docker-compose.dev.yml` и поднимаем сервисы: +You only need to execute `go run main.go` to run the project, but without Redis database and a secret key it won't work +for very long. You have to export `CHRLY_SECRET` environment variable globally or pass it via `env`: ```sh -cp docker/docker-compose.dev.yml docker-compose.yml +env CHRLY_SECRET=some_local_secret go run main.go serve +``` + +Redis can be installed manually, but if you have [Docker installed](https://docs.docker.com/install/), you can run +predefined docker-compose service. Simply execute the next commands: + +```sh +cp docker-compose.dev.yml docker-compose.yml docker-compose up -d ``` -После этого `go run main.go serve` должен запустить web-сервер без дополнительной модификации файла конфигурации. +If your Redis instance isn't located at the `localhost`, you can change host by editing environment variable +`STORAGE_REDIS_HOST`. + +After all of that `go run main.go serve` should successfully start the application. +To run tests execute `go test ./...`. If your Go version is older than 1.9, then run a `/script/test`. From acd0237facfce2550ccc3a5aaa0a028c40039147 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Fri, 16 Feb 2018 00:01:46 +0300 Subject: [PATCH 23/26] Update Dockerfile, add docker-compose for prod and dev environment, cleanup some old things --- .dockerignore | 3 --- .gitignore | 18 ++++-------------- Dockerfile | 12 ++++++++++++ config.dist.yml | 31 ------------------------------- docker-compose.dev.yml | 14 ++++++++++++++ docker-compose.prod.yml | 27 +++++++++++++++++++++++++++ docker-entrypoint.sh | 12 ++++++++++++ docker/Dockerfile | 13 ------------- docker/config.dist.yml | 31 ------------------------------- docker/docker-compose.dev.yml | 29 ----------------------------- docker/docker-compose.prod.yml | 18 ------------------ docker/docker-entrypoint.sh | 15 --------------- 12 files changed, 69 insertions(+), 154 deletions(-) create mode 100644 Dockerfile delete mode 100644 config.dist.yml create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.prod.yml create mode 100755 docker-entrypoint.sh delete mode 100644 docker/Dockerfile delete mode 100644 docker/config.dist.yml delete mode 100644 docker/docker-compose.dev.yml delete mode 100644 docker/docker-compose.prod.yml delete mode 100755 docker/docker-entrypoint.sh diff --git a/.dockerignore b/.dockerignore index eab7932..b9546b0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,2 @@ -# Игнорим данные, т.к. они не нужны для внутреннего содержимого этого контейнера data - -# Vendor так же не нужен vendor diff --git a/.gitignore b/.gitignore index 55118de..efa479e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,5 @@ -# IDEA -/.idea - -# Docker Compose file -/docker-compose.yml -/docker-compose.override.yml - -# vendor -/vendor - -# Cover output +.idea +docker-compose.yml +docker-compose.override.yml +vendor .cover - -# Local config -/config.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..eb83c07 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM alpine:3.7 + +EXPOSE 80 + +ENV STORAGE_REDIS_HOST=redis +ENV STORAGE_FILESYSTEM_HOST=/data + +COPY docker-entrypoint.sh /usr/local/bin/ +COPY release/chrly /usr/local/bin/ + +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["serve"] diff --git a/config.dist.yml b/config.dist.yml deleted file mode 100644 index ef890c7..0000000 --- a/config.dist.yml +++ /dev/null @@ -1,31 +0,0 @@ -# Main server configuration. Actually you don't want to change it, -# but you able to change host or port, that will be used by serve command -server: - host: localhost - port: 80 - -# Both of web or worker depends on storage. -storage: - # For now app require Redis and don't support any other backends to store - # skins, but in the future we can have more backends. Poll size tune amount - # of connections to the redis. It's not recommended to set it less then 2 - # because it will lead to panic on high load. - redis: - host: localhost - port: 6379 - poolSize: 10 - - # Filesystem storage used to store capes. basePath specify absolute or relative - # path to storage and capesDirName specify which folder in this base path will - # be used to search capes. - filesystem: - basePath: data - capesDirName: capes - -# StatsD can be used to collect metrics -# statsd: -# addr: localhost:3746 - -# Sentry can be used to collect app errors -# sentry: -# dsn: "https://public:private@your.sentry.io/1" diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..20e6f82 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,14 @@ +# This file can be used to start up necessary services. +# Copy it into the docker-compose.yml: +# > cp docker-compose.dev.yml docker-compose.yml +# And then run it: +# > docker-compose up -d + +version: '2' +services: + redis: + image: redis:4.0-32bit + ports: + - "6379:6379" + volumes: + - ./data/redis:/data diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..ca4e940 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,27 @@ +# This file can be used to run application in the production environment. +# Copy it into the docker-compose.yml: +# > cp docker-compose.prod.yml docker-compose.yml +# And then run it: +# > docker-compose up -d +# Service will be listened at the http://localhost + +version: '2' +services: + app: + image: elyby/chrly + hostname: chrly0 + restart: always + links: + - redis + volumes: + - ./data/capes:/data/capes + ports: + - "80:80" + environment: + CHRLY_SECRET: replace_this_value_in_production + + redis: + image: redis:4.0-32bit # 32-bit version is recommended to spare some memory + restart: always + volumes: + - ./data/redis:/data diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..de3fa34 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +if [ ! -d /data/capes ]; then + mkdir -p /data/capes +fi + +if [ "$1" = "serve" ] || [ "$1" = "token" ] || [ "$1" = "version" ]; then + set -- /usr/local/bin/chrly "$@" +fi + +exec "$@" diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index dc6d420..0000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM alpine:3.6 - -RUN apk --update add ca-certificates \ - && update-ca-certificates \ - && rm -rf /var/cache/apk/* - -COPY docker/docker-entrypoint.sh /usr/local/bin/ -COPY docker/config.dist.yml /usr/local/etc/minecraft-skinsystem/ - -COPY minecraft-skinsystem /usr/local/bin/ - -ENTRYPOINT ["docker-entrypoint.sh"] -CMD ["serve"] diff --git a/docker/config.dist.yml b/docker/config.dist.yml deleted file mode 100644 index 2827222..0000000 --- a/docker/config.dist.yml +++ /dev/null @@ -1,31 +0,0 @@ -# Main server configuration. Actually you don't want to change it, -# but you able to change host or port, that will be used by serve command -server: - host: # leave host empty to allow Docker publish port - port: 80 - -# Both of web or worker depends on storage. -storage: - # For now app require Redis and don't support any other backends to store - # skins, but in the future we can have more backends. Poll size tune amount - # of connections to the redis. It's not recommended to set it less then 2 - # because it will lead to panic on high load. - redis: - host: redis - port: 6379 - poolSize: 10 - - # Filesystem storage used to store capes. basePath specify absolute or relative - # path to storage and capesDirName specify which folder in this base path will - # be used to search capes. - filesystem: - basePath: /data - capesDirName: capes - -# StatsD can be used to collect metrics -# statsd: -# addr: localhost:3746 - -# Sentry can be used to collect app errors -# sentry: -# dsn: https://public:private@your.sentry.io/1 diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml deleted file mode 100644 index 1533b4b..0000000 --- a/docker/docker-compose.dev.yml +++ /dev/null @@ -1,29 +0,0 @@ -# This compose file contains necessary docker-compose config to quick start -# services required by app. Ports published to host. -# -# Usage: -# 1. Clone this file as docker-compose.yml: -# cp docker/docker-compose.dev.yml docker-compose.yml -# -# 2. If necessary, then you can fix configuration to your environment. -# Then start all services: -# docker-compose up -d -# -# 3. Pass to the project configuration links to this services: -# storage: -# redis: -# host: localhost -# port: 6379 -# poolSize: 10 -# -# 4. After job is done all services can be stopped: -# docker-compose stop - -version: '2' -services: - redis: - image: redis:3.2-32bit - ports: - - "6379:6379" - volumes: - - ./data/redis:/data diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml deleted file mode 100644 index a94f397..0000000 --- a/docker/docker-compose.prod.yml +++ /dev/null @@ -1,18 +0,0 @@ -version: '2' -services: - web: - image: registry.ely.by/elyby/skinsystem:latest - restart: always - ports: - - "80:80" - links: - - redis - volumes: - - ./data/capes:/data/capes - - ./config/minecraft-skinsystem:/etc/minecraft-skinsystem - - redis: - image: redis:3.2-32bit # 32-bit version used to decrease memory usage - restart: always - volumes: - - ./data/redis:/data diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh deleted file mode 100755 index 9382f91..0000000 --- a/docker/docker-entrypoint.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh -set -e - -CONFIG="/etc/minecraft-skinsystem/config.yml" - -if [ ! -f "$CONFIG" ]; then - mkdir -p $(dirname "${CONFIG}") - cp /usr/local/etc/minecraft-skinsystem/config.dist.yml "$CONFIG" -fi - -if [ "$1" = "serve" ] || [ "$1" = "token" ] || [ "$1" = "version" ]; then - set -- minecraft-skinsystem "$@" -fi - -exec "$@" From 11647f2eae9fbae7853d6a2ad8305124323ffaa6 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Fri, 16 Feb 2018 00:02:05 +0300 Subject: [PATCH 24/26] Remove gitlab-ci --- .gitlab-ci.yml | 96 -------------------------------------------------- 1 file changed, 96 deletions(-) delete mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 4c4aec3..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,96 +0,0 @@ -# Предполагается, что между работой "build docker container" и этапом push -# построенные docker images остаются статичными и никуда не пропадают -# -# В противном случае их нужно после каждого этапа билда пушить в registry - -stages: - - test - - build - - build_docker_image - - push - - cleanup - -variables: - CONTAINER_IMAGE: registry.ely.by/elyby/skinsystem - -.golang_template: &setup_go_environment - image: golang:1.9.0-alpine3.6 - before_script: - - apk add --no-cache git - - mkdir -p $GOPATH/src/$CI_PROJECT_NAMESPACE - - cp -r $(pwd) $GOPATH/src/$CI_PROJECT_PATH - - cd $GOPATH/src/$CI_PROJECT_PATH - - go get -u github.com/golang/dep/cmd/dep - - $GOPATH/bin/dep ensure - -.docker_template: &setup_docker_environment - image: docker:latest - before_script: - - docker login -u gitlab-ci -p $CI_JOB_TOKEN registry.ely.by - - export TEMP_IMAGE_NAME="$CONTAINER_IMAGE:$CI_PIPELINE_ID" - -test: - <<: *setup_go_environment - stage: test - script: - - ./script/coverage - -build executable: - <<: *setup_go_environment - stage: build - script: - - export VERSION="${CI_COMMIT_TAG:-dev-$CI_COMMIT_REF_NAME-${CI_COMMIT_SHA:0:8}+build-$CI_JOB_ID}" - - > - env GOOS=linux - go build - -o $CI_PROJECT_DIR/minecraft-skinsystem - -ldflags "-X ${CI_PROJECT_PATH}/bootstrap.version=${VERSION}" - main.go - artifacts: - name: "${CI_JOB_STAGE} executable" - paths: - - $CI_PROJECT_DIR/minecraft-skinsystem - expire_in: 1 day - -build docker image: - <<: *setup_docker_environment - stage: build_docker_image - script: - - docker build -t $TEMP_IMAGE_NAME -f docker/Dockerfile . - only: - - tags - - develop - -push dev: - <<: *setup_docker_environment - stage: push - variables: - GIT_STRATEGY: none - script: - - export IMAGE_NAME="$CONTAINER_IMAGE:dev" - - docker tag $TEMP_IMAGE_NAME $IMAGE_NAME - - docker push $IMAGE_NAME - only: - - develop - -push tag: - <<: *setup_docker_environment - stage: push - variables: - GIT_STRATEGY: none - script: - - export IMAGE_NAME="$CONTAINER_IMAGE:$CI_COMMIT_TAG" - - export LATEST_IMAGE_NAME="$CONTAINER_IMAGE:latest" - - docker tag $TEMP_IMAGE_NAME $IMAGE_NAME - - docker tag $TEMP_IMAGE_NAME $LATEST_IMAGE_NAME - - docker push $IMAGE_NAME - - docker push $LATEST_IMAGE_NAME - only: - - tags - -cleanup temp image: - <<: *setup_docker_environment - stage: cleanup - when: always - script: - - docker rmi $TEMP_IMAGE_NAME || true From ce4dce49a2a4a4068e9b1ef51153d65ddd38c674 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Fri, 16 Feb 2018 00:13:57 +0300 Subject: [PATCH 25/26] Completely rename project to the Chrly and make it ready to be opensourced --- Gopkg.toml | 2 +- cmd/root.go | 2 +- cmd/serve.go | 8 ++++---- cmd/token.go | 2 +- cmd/version.go | 2 +- db/factory.go | 2 +- db/filesystem.go | 4 ++-- db/redis.go | 4 ++-- http/api.go | 8 ++++---- http/api_test.go | 4 ++-- http/cape_test.go | 4 ++-- http/http.go | 2 +- http/http_test.go | 4 ++-- http/signed_textures_test.go | 2 +- http/skin_test.go | 4 ++-- http/textures.go | 2 +- http/textures_test.go | 4 ++-- interfaces/mock_interfaces/mock_interfaces.go | 2 +- interfaces/repositories.go | 2 +- main.go | 2 +- 20 files changed, 33 insertions(+), 33 deletions(-) diff --git a/Gopkg.toml b/Gopkg.toml index 9e6ac7d..868b732 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -1,4 +1,4 @@ -ignored = ["elyby/minecraft-skinsystem"] +ignored = ["github.com/elyby/chrly"] [[constraint]] name = "github.com/gorilla/mux" diff --git a/cmd/root.go b/cmd/root.go index de5fcf5..5b973bd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,7 +5,7 @@ import ( "os" "strings" - "elyby/minecraft-skinsystem/bootstrap" + "github.com/elyby/chrly/bootstrap" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/serve.go b/cmd/serve.go index 689afbc..32794eb 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -4,14 +4,14 @@ import ( "fmt" "log" - "elyby/minecraft-skinsystem/auth" + "github.com/elyby/chrly/auth" "github.com/spf13/cobra" "github.com/spf13/viper" - "elyby/minecraft-skinsystem/bootstrap" - "elyby/minecraft-skinsystem/db" - "elyby/minecraft-skinsystem/http" + "github.com/elyby/chrly/bootstrap" + "github.com/elyby/chrly/db" + "github.com/elyby/chrly/http" ) var serveCmd = &cobra.Command{ diff --git a/cmd/token.go b/cmd/token.go index b74829e..2380d36 100644 --- a/cmd/token.go +++ b/cmd/token.go @@ -4,7 +4,7 @@ import ( "fmt" "log" - "elyby/minecraft-skinsystem/auth" + "github.com/elyby/chrly/auth" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/version.go b/cmd/version.go index 6b50b39..e1196fe 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/spf13/cobra" - "elyby/minecraft-skinsystem/bootstrap" + "github.com/elyby/chrly/bootstrap" "runtime" ) diff --git a/db/factory.go b/db/factory.go index 62eaba5..93f88c8 100644 --- a/db/factory.go +++ b/db/factory.go @@ -3,7 +3,7 @@ package db import ( "github.com/spf13/viper" - "elyby/minecraft-skinsystem/interfaces" + "github.com/elyby/chrly/interfaces" ) type StorageFactory struct { diff --git a/db/filesystem.go b/db/filesystem.go index 376c167..cbc6251 100644 --- a/db/filesystem.go +++ b/db/filesystem.go @@ -5,8 +5,8 @@ import ( "path" "strings" - "elyby/minecraft-skinsystem/interfaces" - "elyby/minecraft-skinsystem/model" + "github.com/elyby/chrly/interfaces" + "github.com/elyby/chrly/model" ) type FilesystemFactory struct { diff --git a/db/redis.go b/db/redis.go index e3e9d70..08f1359 100644 --- a/db/redis.go +++ b/db/redis.go @@ -14,8 +14,8 @@ import ( "github.com/mediocregopher/radix.v2/redis" "github.com/mediocregopher/radix.v2/util" - "elyby/minecraft-skinsystem/interfaces" - "elyby/minecraft-skinsystem/model" + "github.com/elyby/chrly/interfaces" + "github.com/elyby/chrly/model" ) type RedisFactory struct { diff --git a/http/api.go b/http/api.go index f81c6e3..ac737f1 100644 --- a/http/api.go +++ b/http/api.go @@ -8,10 +8,10 @@ import ( "regexp" "strconv" - "elyby/minecraft-skinsystem/auth" - "elyby/minecraft-skinsystem/db" - "elyby/minecraft-skinsystem/interfaces" - "elyby/minecraft-skinsystem/model" + "github.com/elyby/chrly/auth" + "github.com/elyby/chrly/db" + "github.com/elyby/chrly/interfaces" + "github.com/elyby/chrly/model" "github.com/gorilla/mux" "github.com/mono83/slf/wd" diff --git a/http/api_test.go b/http/api_test.go index 34fc532..e0f2fee 100644 --- a/http/api_test.go +++ b/http/api_test.go @@ -10,8 +10,8 @@ import ( "net/url" "testing" - "elyby/minecraft-skinsystem/auth" - "elyby/minecraft-skinsystem/db" + "github.com/elyby/chrly/auth" + "github.com/elyby/chrly/db" "github.com/golang/mock/gomock" testify "github.com/stretchr/testify/assert" diff --git a/http/cape_test.go b/http/cape_test.go index 1d3de99..fe20a48 100644 --- a/http/cape_test.go +++ b/http/cape_test.go @@ -11,8 +11,8 @@ import ( "github.com/golang/mock/gomock" testify "github.com/stretchr/testify/assert" - "elyby/minecraft-skinsystem/db" - "elyby/minecraft-skinsystem/model" + "github.com/elyby/chrly/db" + "github.com/elyby/chrly/model" ) func TestConfig_Cape(t *testing.T) { diff --git a/http/http.go b/http/http.go index 41d6662..a539a15 100644 --- a/http/http.go +++ b/http/http.go @@ -13,7 +13,7 @@ import ( "github.com/gorilla/mux" "github.com/mono83/slf/wd" - "elyby/minecraft-skinsystem/interfaces" + "github.com/elyby/chrly/interfaces" ) type Config struct { diff --git a/http/http_test.go b/http/http_test.go index 45fdac3..884899a 100644 --- a/http/http_test.go +++ b/http/http_test.go @@ -6,8 +6,8 @@ import ( "github.com/golang/mock/gomock" testify "github.com/stretchr/testify/assert" - "elyby/minecraft-skinsystem/interfaces/mock_interfaces" - "elyby/minecraft-skinsystem/interfaces/mock_wd" + "github.com/elyby/chrly/interfaces/mock_interfaces" + "github.com/elyby/chrly/interfaces/mock_wd" ) func TestParseUsername(t *testing.T) { diff --git a/http/signed_textures_test.go b/http/signed_textures_test.go index 3a3a9bf..41934f5 100644 --- a/http/signed_textures_test.go +++ b/http/signed_textures_test.go @@ -8,7 +8,7 @@ import ( "github.com/golang/mock/gomock" testify "github.com/stretchr/testify/assert" - "elyby/minecraft-skinsystem/db" + "github.com/elyby/chrly/db" ) func TestConfig_SignedTextures(t *testing.T) { diff --git a/http/skin_test.go b/http/skin_test.go index c58585d..1540171 100644 --- a/http/skin_test.go +++ b/http/skin_test.go @@ -7,8 +7,8 @@ import ( "github.com/golang/mock/gomock" testify "github.com/stretchr/testify/assert" - "elyby/minecraft-skinsystem/db" - "elyby/minecraft-skinsystem/model" + "github.com/elyby/chrly/db" + "github.com/elyby/chrly/model" ) func TestConfig_Skin(t *testing.T) { diff --git a/http/textures.go b/http/textures.go index aa58529..a07e0c8 100644 --- a/http/textures.go +++ b/http/textures.go @@ -11,7 +11,7 @@ import ( "github.com/gorilla/mux" - "elyby/minecraft-skinsystem/model" + "github.com/elyby/chrly/model" ) type texturesResponse struct { diff --git a/http/textures_test.go b/http/textures_test.go index 9fdbe09..c4c879f 100644 --- a/http/textures_test.go +++ b/http/textures_test.go @@ -10,8 +10,8 @@ import ( "github.com/golang/mock/gomock" testify "github.com/stretchr/testify/assert" - "elyby/minecraft-skinsystem/db" - "elyby/minecraft-skinsystem/model" + "github.com/elyby/chrly/db" + "github.com/elyby/chrly/model" ) func TestConfig_Textures(t *testing.T) { diff --git a/interfaces/mock_interfaces/mock_interfaces.go b/interfaces/mock_interfaces/mock_interfaces.go index 78744c4..846ec92 100644 --- a/interfaces/mock_interfaces/mock_interfaces.go +++ b/interfaces/mock_interfaces/mock_interfaces.go @@ -4,7 +4,7 @@ package mock_interfaces import ( - model "elyby/minecraft-skinsystem/model" + model "github.com/elyby/chrly/model" gomock "github.com/golang/mock/gomock" reflect "reflect" ) diff --git a/interfaces/repositories.go b/interfaces/repositories.go index 5fdca61..05d2df5 100644 --- a/interfaces/repositories.go +++ b/interfaces/repositories.go @@ -1,7 +1,7 @@ package interfaces import ( - "elyby/minecraft-skinsystem/model" + "github.com/elyby/chrly/model" ) type SkinsRepository interface { diff --git a/main.go b/main.go index 5640a87..1a95996 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,7 @@ package main import ( "runtime" - "elyby/minecraft-skinsystem/cmd" + "github.com/elyby/chrly/cmd" ) func main() { From 87a302c7dacf2ac07788c0df03b97cef16cfb1dd Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Fri, 16 Feb 2018 00:46:20 +0300 Subject: [PATCH 26/26] Hello, Travis? --- .travis.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2d65eb3 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,34 @@ +sudo: required + +language: go +go: + - 1.9 + +services: + - docker + +stages: + - test + - publish + +before_install: + - go get -u github.com/golang/dep/cmd/dep + +jobs: + include: + - stage: test + script: + - dep ensure + - go test ./... + - stage: publish + script: + - docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" + - dep ensure + - > + env GOOS=linux + go build + -o release/chrly + -ldflags "-X github.com/elyby/chrly/bootstrap.version=latest" + main.go + - docker build -t elyby/chrly . + - docker push elyby/chrly