diff --git a/config.default.yml b/config.default.yml index 91ad61d..095edec 100644 --- a/config.default.yml +++ b/config.default.yml @@ -8,6 +8,7 @@ database: username: root features: allow_register: false + password_hash_cost: 16 path: cache: "/var/osc/cache" storage: "/var/osc/storage" \ No newline at end of file diff --git a/config/config.go b/config/config.go index 638defe..d4e817f 100644 --- a/config/config.go +++ b/config/config.go @@ -2,6 +2,7 @@ package config import ( "flag" + "golang.org/x/crypto/bcrypt" "gopkg.in/yaml.v3" "log" "os" @@ -31,7 +32,8 @@ type DatabaseConfiguration struct { } type FeaturesConfiguration struct { - AllowRegister bool `yaml:"allow_register"` + AllowRegister bool `yaml:"allow_register"` + PasswordHashCost *int `yaml:"password_hash_cost"` } var currentConfig *Configuration @@ -47,6 +49,22 @@ func init() { if err != nil { log.Fatalf("error: %s", err) } + checkConfig() +} + +func checkConfig() { + if currentConfig.Features.PasswordHashCost == nil { + currentConfig.Features.PasswordHashCost = new(int) + *currentConfig.Features.PasswordHashCost = bcrypt.DefaultCost + } else if *currentConfig.Features.PasswordHashCost < bcrypt.MinCost && *currentConfig.Features.PasswordHashCost > bcrypt.MaxCost { + log.Fatalf("password_hash_cost is not on the supported range (%d < x < %d)", bcrypt.MinCost, bcrypt.MaxCost) + } + if _, err := os.Stat(currentConfig.Path.Storage); err != nil { + log.Fatal(err) + } + if _, err := os.Stat(currentConfig.Path.Cache); err != nil { + log.Fatal(err) + } } func Database() *DatabaseConfiguration { diff --git a/database/database.go b/database/database.go index 5801864..ea61cca 100644 --- a/database/database.go +++ b/database/database.go @@ -1,31 +1,23 @@ package database import ( - "errors" "fmt" "github.com/google/uuid" + "golang.org/x/crypto/bcrypt" "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/logger" - "io" "log" - "mime/multipart" "opensavecloudserver/config" "os" - "path" - "sync" "time" ) -var ( - locks map[int]GameUploadToken - mu sync.Mutex -) - var db *gorm.DB +const adminRole string = "admin" + func init() { - locks = make(map[int]GameUploadToken) dbConfig := config.Database() var err error connectionString := "" @@ -55,12 +47,6 @@ func init() { if err != nil { log.Fatal(err) } - go func() { - for { - time.Sleep(time.Minute) - clearLocks() - } - }() } // UserByUsername get a user by the username @@ -70,6 +56,9 @@ func UserByUsername(username string) (*User, error) { if err != nil { return nil, err } + if user.Role == adminRole { + user.IsAdmin = true + } return user, nil } @@ -80,6 +69,9 @@ func UserById(userId int) (*User, error) { if err != nil { return nil, err } + if user.Role == adminRole { + user.IsAdmin = true + } return user, nil } @@ -92,6 +84,16 @@ func AddUser(username string, password []byte) error { return db.Save(user).Error } +// AddAdmin register a user and set his role to admin +func AddAdmin(username string, password []byte) error { + user := &User{ + Username: username, + Password: password, + Role: adminRole, + } + return db.Save(user).Error +} + // GameInfoById return information of a game func GameInfoById(userId, gameId int) (*Game, error) { var game *Game @@ -128,51 +130,6 @@ func CreateGame(userId int, name string) (*Game, error) { return game, nil } -// AskForUpload Create a lock for upload a new revision of a game -func AskForUpload(userId, gameId int) (*GameUploadToken, error) { - mu.Lock() - defer mu.Unlock() - _, err := GameInfoById(userId, gameId) - if err != nil { - return nil, err - } - if _, ok := locks[gameId]; !ok { - token := uuid.New() - lock := GameUploadToken{ - GameId: gameId, - UploadToken: token.String(), - } - locks[gameId] = lock - return &lock, nil - } - return nil, errors.New("game already locked") -} - -func CheckUploadToken(uploadToken string) (int, bool) { - mu.Lock() - defer mu.Unlock() - for _, lock := range locks { - if lock.UploadToken == uploadToken { - return lock.GameId, true - } - } - return -1, false -} - -func UploadSave(file multipart.File, game *Game) error { - filePath := path.Join(config.Path().Storage, game.PathStorage) - f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - return err - } - defer f.Close() - _, err = io.Copy(f, file) - if err != nil { - return err - } - return nil -} - func UpdateGameRevision(game *Game, hash string) error { game.Revision += 1 if game.Hash == nil { @@ -191,24 +148,20 @@ func UpdateGameRevision(game *Game, hash string) error { return nil } -func UnlockGame(gameId int) { - mu.Lock() - defer mu.Unlock() - delete(locks, gameId) -} - -// clearLocks clear lock of zombi upload -func clearLocks() { - mu.Lock() - defer mu.Unlock() - now := time.Now() - toUnlock := make([]int, 0) - for gameId, lock := range locks { - if lock.Expire.After(now) { - toUnlock = append(toUnlock, gameId) - } +// ChangePassword change the password of the user, the param 'password' must be the clear password +func ChangePassword(userId int, password []byte) error { + user, err := UserById(userId) + if err != nil { + return err } - for _, gameId := range toUnlock { - delete(locks, gameId) + hashedPassword, err := bcrypt.GenerateFromPassword(password, *config.Features().PasswordHashCost) + if err != nil { + return err } + user.Password = hashedPassword + err = db.Save(user).Error + if err != nil { + return err + } + return nil } diff --git a/database/model.go b/database/model.go index aad24a1..7d41792 100644 --- a/database/model.go +++ b/database/model.go @@ -5,7 +5,9 @@ import "time" type User struct { ID int `json:"id"` Username string `json:"username"` + Role string `json:"role"` Password []byte `json:"-"` + IsAdmin bool `json:"is_admin" gorm:"-:all"` } type Game struct { @@ -18,9 +20,3 @@ type Game struct { UserId int `json:"-"` Available bool `json:"available"` } - -type GameUploadToken struct { - GameId int `json:"-"` - UploadToken string `json:"upload_token"` - Expire time.Time `json:"expire"` -} diff --git a/server/data.go b/server/data.go index 914d008..98dcb2a 100644 --- a/server/data.go +++ b/server/data.go @@ -8,6 +8,7 @@ import ( "net/http" "opensavecloudserver/config" "opensavecloudserver/database" + "opensavecloudserver/upload" "os" "path/filepath" "strconv" @@ -27,6 +28,11 @@ type LockError struct { Message string `json:"message"` } +type NewPassword struct { + Password string `json:"password"` + VerifyPassword string `json:"verify_password"` +} + // CreateGame create a game entry to the database func CreateGame(w http.ResponseWriter, r *http.Request) { userId, err := userIdFromContext(r.Context()) @@ -119,7 +125,7 @@ func AskForUpload(w http.ResponseWriter, r *http.Request) { log.Println(err) return } - token, err := database.AskForUpload(userId, gameInfo.GameId) + token, err := upload.AskForUpload(userId, gameInfo.GameId) if err != nil { ok(LockError{Message: err.Error()}, w, r) return @@ -141,7 +147,7 @@ func UploadSave(w http.ResponseWriter, r *http.Request) { log.Println(err) return } - defer database.UnlockGame(gameId) + defer upload.UnlockGame(gameId) hash := r.Header.Get("X-Game-Save-Hash") if utf8.RuneCountInString(hash) == 0 { badRequest("The header X-Game-Save-Hash is missing", w, r) @@ -160,7 +166,7 @@ func UploadSave(w http.ResponseWriter, r *http.Request) { return } defer file.Close() - err = database.UploadSave(file, game) + err = upload.UploadSave(file, game) if err != nil { internalServerError(w, r) log.Println(err) @@ -194,7 +200,7 @@ func Download(w http.ResponseWriter, r *http.Request) { log.Println(err) return } - defer database.UnlockGame(gameId) + defer upload.UnlockGame(gameId) game, err := database.GameInfoById(userId, gameId) if err != nil { internalServerError(w, r) @@ -237,3 +243,41 @@ func UserInformation(w http.ResponseWriter, r *http.Request) { } ok(user, w, r) } + +func ChangePassword(w http.ResponseWriter, r *http.Request) { + userId, err := userIdFromContext(r.Context()) + if err != nil { + internalServerError(w, r) + log.Println(err) + return + } + body, err := io.ReadAll(r.Body) + if err != nil { + internalServerError(w, r) + log.Println(err) + return + } + newPassword := new(NewPassword) + err = json.Unmarshal(body, newPassword) + if err != nil { + internalServerError(w, r) + log.Println(err) + return + } + if newPassword.Password != newPassword.VerifyPassword { + badRequest("password are not the same", w, r) + return + } + err = database.ChangePassword(userId, []byte(newPassword.Password)) + if err != nil { + internalServerError(w, r) + log.Println(err) + return + } + payload := &successMessage{ + Message: "Password changed", + Timestamp: time.Now(), + Status: 200, + } + ok(payload, w, r) +} diff --git a/server/server.go b/server/server.go index 43e4a3c..4c7eb64 100644 --- a/server/server.go +++ b/server/server.go @@ -10,7 +10,7 @@ import ( "net/http" "opensavecloudserver/authentication" "opensavecloudserver/config" - "opensavecloudserver/database" + "opensavecloudserver/upload" ) type ContextKey string @@ -39,6 +39,7 @@ func Serve() { r.Route("/user", func(secureRouter chi.Router) { secureRouter.Use(authMiddleware) secureRouter.Get("/information", UserInformation) + secureRouter.Post("/passwd", ChangePassword) }) r.Route("/game", func(secureRouter chi.Router) { secureRouter.Use(authMiddleware) @@ -86,7 +87,7 @@ func uploadMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { header := r.Header.Get("X-Upload-Key") if len(header) > 0 { - if gameId, ok := database.CheckUploadToken(header); ok { + if gameId, ok := upload.CheckUploadToken(header); ok { ctx := context.WithValue(r.Context(), GameIdKey, gameId) r = r.WithContext(ctx) next.ServeHTTP(w, r) diff --git a/upload/upload.go b/upload/upload.go new file mode 100644 index 0000000..b37307a --- /dev/null +++ b/upload/upload.go @@ -0,0 +1,133 @@ +package upload + +import ( + "errors" + "github.com/google/uuid" + "io" + "mime/multipart" + "opensavecloudserver/config" + "opensavecloudserver/database" + "os" + "path" + "sync" + "time" +) + +var ( + locks map[int]GameUploadToken + mu sync.Mutex +) + +type GameUploadToken struct { + GameId int `json:"-"` + UploadToken string `json:"upload_token"` + Expire time.Time `json:"expire"` +} + +func init() { + locks = make(map[int]GameUploadToken) + go func() { + for { + time.Sleep(time.Minute) + clearLocks() + } + }() +} + +// AskForUpload Create a lock for upload a new revision of a game +func AskForUpload(userId, gameId int) (*GameUploadToken, error) { + mu.Lock() + defer mu.Unlock() + _, err := database.GameInfoById(userId, gameId) + if err != nil { + return nil, err + } + if _, ok := locks[gameId]; !ok { + token := uuid.New() + lock := GameUploadToken{ + GameId: gameId, + UploadToken: token.String(), + } + locks[gameId] = lock + return &lock, nil + } + return nil, errors.New("game already locked") +} + +func CheckUploadToken(uploadToken string) (int, bool) { + mu.Lock() + defer mu.Unlock() + for _, lock := range locks { + if lock.UploadToken == uploadToken { + return lock.GameId, true + } + } + return -1, false +} + +func UploadSave(file multipart.File, game *database.Game) error { + filePath := path.Join(config.Path().Cache, string(rune(game.UserId))) + if _, err := os.Stat(filePath); err != nil { + err = os.Mkdir(filePath, 0766) + if err != nil { + return err + } + } + filePath = path.Join(filePath, game.PathStorage) + f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + return err + } + defer f.Close() + _, err = io.Copy(f, file) + if err != nil { + return err + } + err = moveToStorage(filePath, game) + if err != nil { + return err + } + return nil +} + +func moveToStorage(cachePath string, game *database.Game) error { + filePath := path.Join(config.Path().Storage, string(rune(game.UserId))) + if _, err := os.Stat(filePath); err != nil { + err = os.Mkdir(filePath, 0766) + if err != nil { + return err + } + } + filePath = path.Join(filePath, game.PathStorage) + if _, err := os.Stat(filePath); err == nil { + if err = os.Remove(filePath); err != nil { + return err + } + } + if err := os.Rename(cachePath, filePath); err != nil { + return err + } + return nil +} + +func UnlockGame(gameId int) { + mu.Lock() + defer mu.Unlock() + delete(locks, gameId) +} + +// clearLocks clear lock of zombi upload +func clearLocks() { + mu.Lock() + defer mu.Unlock() + now := time.Now() + toUnlock := make([]int, 0) + for gameId, lock := range locks { + if lock.Expire.After(now) { + toUnlock = append(toUnlock, gameId) + } + } + for _, gameId := range toUnlock { + delete(locks, gameId) + } +}